fix: support mixed wall and game clock servers

This commit is contained in:
2026-08-04 01:09:18 +00:00
parent 41ec80c96b
commit 68a82c066b
8 changed files with 153 additions and 25 deletions
+52 -7
View File
@@ -43,13 +43,10 @@ final class GameClock
public static function fromStorage(KVStorage $gameStor, ?callable $wallNowProvider = null): self
{
$values = $gameStor->getValues([
'clock_base_time',
'clock_tick',
'clock_mode',
'clock_wall_anchor',
'turnterm',
]);
$values = self::readStorageValues($gameStor);
if (!self::areStorageValuesInitialized($values)) {
throw new \RuntimeException('game clock storage가 초기화되지 않았습니다. migration 상태를 확인해 주세요.');
}
$baseTime = new \DateTimeImmutable((string)$values['clock_base_time']);
$wallAnchor = new \DateTimeImmutable((string)$values['clock_wall_anchor']);
@@ -64,6 +61,54 @@ final class GameClock
);
}
/**
* 공용 src가 아직 migration하지 않은 wall-clock profile과 함께 배포될 수
* 있으므로 호출부가 저장 형식을 먼저 판별할 수 있게 합니다.
*/
public static function isInitialized(KVStorage $gameStor): bool
{
$values = self::readStorageValues($gameStor);
$clockKeys = ['clock_base_time', 'clock_tick', 'clock_mode', 'clock_wall_anchor'];
$presentClockKeys = array_filter(
$clockKeys,
static fn (string $key): bool => array_key_exists($key, $values)
&& $values[$key] !== null
&& $values[$key] !== '',
);
if (!$presentClockKeys) {
return false;
}
if (!self::areStorageValuesInitialized($values)) {
throw new \RuntimeException('game clock storage가 부분 초기화 상태입니다. migration 상태를 확인해 주세요.');
}
return true;
}
/** @return array<string, mixed> */
private static function readStorageValues(KVStorage $gameStor): array
{
return $gameStor->getValues([
'clock_base_time',
'clock_tick',
'clock_mode',
'clock_wall_anchor',
'turnterm',
]);
}
/** @param array<string, mixed> $values */
private static function areStorageValuesInitialized(array $values): bool
{
foreach (['clock_base_time', 'clock_tick', 'clock_mode', 'clock_wall_anchor', 'turnterm'] as $key) {
if (!array_key_exists($key, $values) || $values[$key] === null || $values[$key] === '') {
return false;
}
}
return in_array((string)$values['clock_mode'], [self::MODE_REALTIME, self::MODE_MANUAL], true)
&& is_numeric($values['clock_tick'])
&& is_numeric($values['turnterm']);
}
public static function initializeStorage(
KVStorage $gameStor,
\DateTimeInterface $baseTime,
+27 -11
View File
@@ -36,6 +36,7 @@ class Session
const GAME_KEY_GENERAL_ID = '_g_no';
const GAME_KEY_GENERAL_NAME = '_g_name';
const GAME_KEY_EXPECTED_DEADTIME = '_g_deadtime';
const GAME_KEY_CLOCK_STORAGE = '_g_clock_storage';
protected $writeClosed = false;
@@ -239,16 +240,22 @@ class Session
$loginDate = $this->get($serverID.static::GAME_KEY_DATE);
$generalID = $this->get($serverID.static::GAME_KEY_GENERAL_ID);
$generalName = $this->get($serverID.static::GAME_KEY_GENERAL_NAME);
$deadTick = $this->get($serverID.static::GAME_KEY_EXPECTED_DEADTIME);
$deadAt = $this->get($serverID.static::GAME_KEY_EXPECTED_DEADTIME);
$sessionClockStorage = $this->get($serverID.static::GAME_KEY_CLOCK_STORAGE);
$wallNow = time();
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$gameNowTick = GameClock::fromStorage($gameStor)->nowTick();
$usesLogicalClock = GameClock::isInitialized($gameStor);
$clockStorage = $usesLogicalClock ? 'logical' : 'wall';
$gameNow = $usesLogicalClock
? GameClock::fromStorage($gameStor)->nowTick()
: $wallNow;
if (
$globalLoginDate < $loginDate &&
$generalID && $generalName && $loginDate && $deadTick
&& $loginDate + 1800 > $wallNow && $deadTick > $gameNowTick
$generalID && $generalName && $loginDate && $deadAt
&& $sessionClockStorage === $clockStorage
&& $loginDate + 1800 > $wallNow && $deadAt > $gameNow
) {
//로그인 정보는 30분간 유지한다.
if ($result !== null) {
@@ -257,7 +264,7 @@ class Session
return $this;
}
if ($generalID || $generalName || $loginDate || $deadTick) {
if ($generalID || $generalName || $loginDate || $deadAt) {
$this->logoutGame();
}
@@ -276,11 +283,18 @@ class Session
$generalID = $general['no'];
$generalName = $general['name'];
$deadTick = GameClock::addTicks(
Util::toInt($general['turntime']),
Util::toInt($general['killturn']) * GameClock::TICKS_PER_TURN,
);
if ($deadTick < $gameNowTick && !$isUnited) {
if ($usesLogicalClock) {
$deadAt = GameClock::addTicks(
Util::toInt($general['turntime']),
Util::toInt($general['killturn']) * GameClock::TICKS_PER_TURN,
);
} else {
// migration 전 profile은 기존 DATETIME과 wall-clock 판정을 그대로
// 유지합니다. 공용 src 배포가 기존 서버의 DB 형식을 바꾸지 않습니다.
$deadAt = (new \DateTimeImmutable((string)$general['turntime']))->getTimestamp()
+ Util::toInt($general['killturn']) * Util::toInt($gameStor->turnterm);
}
if ($deadAt < $gameNow && !$isUnited) {
$locked = $db->queryFirstField('SELECT plock FROM plock WHERE `type` = "GAME" LIMIT 1');
if (!$locked) {
if ($result !== null) {
@@ -293,7 +307,8 @@ class Session
$this->set($serverID.static::GAME_KEY_DATE, $wallNow);
$this->set($serverID.static::GAME_KEY_GENERAL_ID, $generalID);
$this->set($serverID.static::GAME_KEY_GENERAL_NAME, $generalName);
$this->set($serverID.static::GAME_KEY_EXPECTED_DEADTIME, $deadTick);
$this->set($serverID.static::GAME_KEY_EXPECTED_DEADTIME, $deadAt);
$this->set($serverID.static::GAME_KEY_CLOCK_STORAGE, $clockStorage);
return $this;
}
@@ -307,6 +322,7 @@ class Session
$this->set($serverID.static::GAME_KEY_GENERAL_ID, null);
$this->set($serverID.static::GAME_KEY_GENERAL_NAME, null);
$this->set($serverID.static::GAME_KEY_EXPECTED_DEADTIME, null);
$this->set($serverID.static::GAME_KEY_CLOCK_STORAGE, null);
return $this;
}