From ed3c84aa8ebe176dc00db7b12f8da9b78c1ae480 Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 3 Aug 2026 05:13:47 +0000 Subject: [PATCH] feat: replace game schedules with logical ticks --- hwe/_119.php | 7 +- hwe/_119_b.php | 45 +-- hwe/_admin1.php | 4 +- hwe/_admin1_submit.php | 9 +- hwe/_admin2_submit.php | 2 +- hwe/_admin7.php | 5 +- hwe/b_currentCity.php | 4 +- hwe/b_genList.php | 4 +- hwe/b_myPage.php | 20 +- hwe/c_tournament.php | 6 +- hwe/func.php | 212 ++++++----- hwe/func_auction.php | 7 +- hwe/func_gamerule.php | 20 +- hwe/func_legacy.php | 4 + hwe/func_template.php | 8 +- hwe/func_tournament.php | 19 +- hwe/j_get_select_npc_token.php | 42 +-- hwe/j_get_select_pool.php | 14 +- hwe/j_install.php | 4 +- hwe/j_select_npc.php | 6 +- hwe/j_select_picked_general.php | 6 +- hwe/j_server_basic_info.php | 7 +- hwe/j_simulate_battle.php | 3 +- hwe/j_update_picked_general.php | 6 +- hwe/proc.php | 6 +- hwe/sammo/API/Auction/BidUniqueAuction.php | 4 +- .../Auction/GetActiveResourceAuctionList.php | 9 +- .../Auction/GetUniqueItemAuctionDetail.php | 9 +- .../API/Auction/GetUniqueItemAuctionList.php | 11 +- hwe/sammo/API/Command/GetReservedCommand.php | 7 +- hwe/sammo/API/General/DieOnPrestart.php | 13 +- hwe/sammo/API/General/GetFrontInfo.php | 20 +- hwe/sammo/API/General/Join.php | 17 +- hwe/sammo/API/Global/ExecuteEngine.php | 6 +- hwe/sammo/API/Global/GeneralList.php | 4 +- hwe/sammo/API/InheritAction/ResetTurnTime.php | 4 +- hwe/sammo/API/Nation/GeneralList.php | 8 +- .../API/NationCommand/GetReservedCommand.php | 4 +- hwe/sammo/AbsFromUserPool.php | 10 +- hwe/sammo/AbsGeneralPool.php | 6 +- hwe/sammo/Auction.php | 109 +++--- hwe/sammo/AuctionBasicResource.php | 16 +- hwe/sammo/AuctionUniqueItem.php | 53 +-- hwe/sammo/Command/Nation/che_발령.php | 2 +- hwe/sammo/Command/Nation/che_천도.php | 2 +- hwe/sammo/DTO/AuctionInfo.php | 12 +- hwe/sammo/DTO/AuctionInfoDetail.php | 5 +- hwe/sammo/DTO/GeneralAccessLog.php | 7 +- hwe/sammo/DiplomaticMessage.php | 4 +- hwe/sammo/General.php | 17 +- hwe/sammo/GeneralAI.php | 27 +- hwe/sammo/GeneralBase.php | 43 ++- hwe/sammo/GeneralPool/RandomNameGeneral.php | 15 +- hwe/sammo/GeneralPool/SPoolUnderU100.php | 2 +- hwe/sammo/Message.php | 47 ++- hwe/sammo/ResetHelper.php | 50 ++- hwe/sammo/Scenario/GeneralBuilder.php | 4 +- hwe/sammo/ScoutMessage.php | 6 +- hwe/sammo/ServerTool.php | 30 +- hwe/sammo/TurnExecutionHelper.php | 22 +- hwe/sammo/WarUnitGeneral.php | 10 +- hwe/sql/schema.sql | 24 +- scripts/README-game-clock.md | 41 +++ scripts/game-clock.php | 129 +++++++ scripts/migrate-game-clock.php | 340 ++++++++++++++++++ scripts/verify-game-clock-engine.php | 76 ++++ src/sammo/GameClock.php | 284 +++++++++++++++ tests/GameClockBoundaryTest.php | 59 +++ tests/GameClockTest.php | 99 +++++ 69 files changed, 1617 insertions(+), 520 deletions(-) create mode 100644 scripts/README-game-clock.md create mode 100644 scripts/game-clock.php create mode 100644 scripts/migrate-game-clock.php create mode 100644 scripts/verify-game-clock-engine.php create mode 100644 src/sammo/GameClock.php create mode 100644 tests/GameClockBoundaryTest.php create mode 100644 tests/GameClockTest.php diff --git a/hwe/_119.php b/hwe/_119.php index 576e1b1c..65de2e5a 100644 --- a/hwe/_119.php +++ b/hwe/_119.php @@ -13,6 +13,9 @@ $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); [$turntime, $tnmt_time] = $gameStor->getValuesAsArray(['turntime','tnmt_time']); +$clock = GameClock::fromStorage($gameStor); +$turntimeDisplay = $clock->formatTick(Util::toInt($turntime), true); +$tnmtTimeDisplay = $tnmt_time === null ? '-' : $clock->formatTick(Util::toInt($tnmt_time), true); $plock = $db->queryFirstField('SELECT plock FROM plock WHERE `type` ="GAME"'); ?> @@ -30,8 +33,8 @@ $plock = $db->queryFirstField('SELECT plock FROM plock WHERE `type` ="GAME"');
- 시간조정 : 최종갱신 :
- 시간조정 : 토너먼트 :
+ 시간조정 : 최종갱신 : (tick )
+ 시간조정 : 토너먼트 : (tick )
봉급지급 :
락 풀 기 : 현재 : 0?"동결중":"가동중"?>
diff --git a/hwe/_119_b.php b/hwe/_119_b.php index 84158b73..18472c12 100644 --- a/hwe/_119_b.php +++ b/hwe/_119_b.php @@ -38,21 +38,9 @@ switch ($btn) { usleep(500000); } - $gameStor->cacheValues(['turntime', 'starttime', 'tnmt_time']); - $turntime = (new \DateTimeImmutable($gameStor->turntime))->sub(new \DateInterval("PT{$minute}M")); - $starttime = (new \DateTimeImmutable($gameStor->starttime))->sub(new \DateInterval("PT{$minute}M")); - $tnmt_time = (new \DateTimeImmutable($gameStor->tnmt_time))->sub(new \DateInterval("PT{$minute}M")); - - $gameStor->turntime = $turntime->format('Y-m-d H:i:s.u'); - $gameStor->starttime = $starttime->format('Y-m-d H:i:s'); - $gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s'); - - $db->update('general', [ - 'turntime' => $db->sqleval('DATE_SUB(turntime, INTERVAL %i MINUTE)', $minute) - ], true); - $db->update('ng_auction', [ - 'close_date' => $db->sqleval('DATE_SUB(close_date, INTERVAL %i MINUTE)', $minute) - ], 'finished = 0'); + $clock = GameClock::fromStorage($gameStor); + // 스케줄 전체를 벽시계에서 빼지 않고 논리 현재 tick만 앞으로 이동합니다. + $clock->advance($gameStor, $clock->ticksFromMinutes($minute)); if ($locked) { unlock(); } @@ -66,34 +54,19 @@ switch ($btn) { } usleep(500000); } - $gameStor->cacheValues(['turntime', 'starttime', 'tnmt_time']); - $turntime = (new \DateTimeImmutable($gameStor->turntime))->add(new \DateInterval("PT{$minute}M")); - $starttime = (new \DateTimeImmutable($gameStor->starttime))->add(new \DateInterval("PT{$minute}M")); - $tnmt_time = (new \DateTimeImmutable($gameStor->tnmt_time))->add(new \DateInterval("PT{$minute}M")); - - $gameStor->turntime = $turntime->format('Y-m-d H:i:s.u'); - $gameStor->starttime = $starttime->format('Y-m-d H:i:s'); - $gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s'); - - $db->update('general', [ - 'turntime' => $db->sqleval('DATE_ADD(turntime, INTERVAL %i MINUTE)', $minute) - ], true); - $db->update('ng_auction', [ - 'close_date' => $db->sqleval('DATE_ADD(close_date, INTERVAL %i MINUTE)', $minute) - ], 'finished = 0'); + $clock = GameClock::fromStorage($gameStor); + $clock->advance($gameStor, -$clock->ticksFromMinutes($minute)); if ($locked) { unlock(); } break; case "토너분당김": - $tnmt_time = new \DateTime($gameStor->tnmt_time); - $tnmt_time->sub(new \DateInterval("PT{$minute2}M")); - $gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s'); + $clock = GameClock::fromStorage($gameStor); + $gameStor->tnmt_time = Util::toInt($gameStor->tnmt_time) - $clock->ticksFromMinutes($minute2); break; case "토너분지연": - $tnmt_time = new \DateTimeImmutable($gameStor->tnmt_time); - $tnmt_time->add(new \DateInterval("PT{$minute2}M")); - $gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s'); + $clock = GameClock::fromStorage($gameStor); + $gameStor->tnmt_time = Util::toInt($gameStor->tnmt_time) + $clock->ticksFromMinutes($minute2); break; case "금지급": processGoldIncome(); diff --git a/hwe/_admin1.php b/hwe/_admin1.php index 069a5ab9..e2a80b8a 100644 --- a/hwe/_admin1.php +++ b/hwe/_admin1.php @@ -40,7 +40,7 @@ $admin = getAdmin(); 시작시간변경 - '> + '> 최대 장수 @@ -52,7 +52,7 @@ $admin = getAdmin(); 시작 년도 '> 최근 갱신 시간 -   +   (tick ) 턴시간 diff --git a/hwe/_admin1_submit.php b/hwe/_admin1_submit.php index 48e66e6a..51827d42 100644 --- a/hwe/_admin1_submit.php +++ b/hwe/_admin1_submit.php @@ -43,7 +43,12 @@ switch ($btn) { pushGlobalHistoryLog(["{$log}"]); break; case "변경1": - $gameStor->starttime = (new \DateTime($starttime))->format('Y-m-d H:i:s'); + $clock = GameClock::fromStorage($gameStor); + $gameStor->clock_base_time = TimeUtil::format(GameClock::baseTimeForProjection( + new \DateTimeImmutable($starttime), + Util::toInt($gameStor->starttime), + $clock->getTurnTermMinutes(), + ), true); break; case "변경2": $gameStor->maxgeneral = $maxgeneral; @@ -78,4 +83,4 @@ switch ($btn) { break; } -header('location:_admin1.php'); \ No newline at end of file +header('location:_admin1.php'); diff --git a/hwe/_admin2_submit.php b/hwe/_admin2_submit.php index 54d63b35..e3ad0652 100644 --- a/hwe/_admin2_submit.php +++ b/hwe/_admin2_submit.php @@ -99,7 +99,7 @@ switch ($btn) { ], '`no` IN %li', $genlist); break; case "강제 사망": - $date = TimeUtil::now(true); + $date = GameClock::fromStorage($gameStor)->nowTick(); $db->update('general', [ 'killturn' => 0, 'turntime' => $date, diff --git a/hwe/_admin7.php b/hwe/_admin7.php index 8623f9c2..c335ee49 100644 --- a/hwe/_admin7.php +++ b/hwe/_admin7.php @@ -42,6 +42,7 @@ if ($session->userGrade < 6) { } $db = DB::db(); +$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); if ($btn == '정렬하기') { $gen = 0; @@ -111,7 +112,7 @@ $generalObj = General::createObjFromDB($gen, null, GeneralQueryMode::FullWithAcc 대상장수 : @@ -179,4 +180,4 @@ $generalObj = General::createObjFromDB($gen, null, GeneralQueryMode::FullWithAcc - \ No newline at end of file + diff --git a/hwe/b_currentCity.php b/hwe/b_currentCity.php index 8df28d80..36615d43 100644 --- a/hwe/b_currentCity.php +++ b/hwe/b_currentCity.php @@ -16,7 +16,7 @@ $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); increaseRefresh("현재도시", 1); -$lastExecute = substr($gameStor->turntime, 5, 14); +$lastExecute = substr(GameClock::fromStorage($gameStor)->formatTick(Util::toInt($gameStor->turntime)), 5, 14); $me = $db->queryFirstRow('SELECT no,nation,officer_level,city from general where owner=%i', $userID); $myNation = $db->queryFirstRow('SELECT nation,level,spy FROM nation WHERE nation=%i', $me['nation']) ?? [ @@ -537,4 +537,4 @@ $templates = new \League\Plates\Engine('templates'); - \ No newline at end of file + diff --git a/hwe/b_genList.php b/hwe/b_genList.php index 0cbae607..59edbd71 100644 --- a/hwe/b_genList.php +++ b/hwe/b_genList.php @@ -129,6 +129,7 @@ $templates = new \League\Plates\Engine('templates'); ); $generalTurnList = []; + $clock = GameClock::fromStorage($gameStor); foreach ($db->queryAllLists( 'SELECT general_id, turn_idx, brief FROM general_turn WHERE general_id IN %li AND turn_idx < 5 ORDER BY general_id ASC, turn_idx ASC', @@ -142,6 +143,7 @@ $templates = new \League\Plates\Engine('templates'); $genCntEff = 0; foreach ($generals as &$general) { + $general['turntime'] = $clock->formatTick(Util::toInt($general['turntime']), true); $general['cityText'] = CityConst::byID($general['city'])->name; $general['troopText'] = $troopName[$general['troop']] ?? '-'; @@ -284,4 +286,4 @@ $templates = new \League\Plates\Engine('templates'); - \ No newline at end of file + diff --git a/hwe/b_myPage.php b/hwe/b_myPage.php index ea3d488d..bbe8a708 100644 --- a/hwe/b_myPage.php +++ b/hwe/b_myPage.php @@ -19,8 +19,10 @@ $userID = Session::getUserID(); $generalID = $session->generalID; $db = DB::db(); -$gameStor = KVStorage::getStorage($db, 'game_env'); -$gameStor->cacheValues(['turntime', 'opentime', 'autorun_user', 'npcmode']); +$gameStor = KVStorage::getStorage($db, 'game_env'); +$gameStor->cacheValues(['turntime', 'opentime', 'autorun_user', 'npcmode']); +$clock = GameClock::fromStorage($gameStor); +$nowTick = $clock->nowTick(); $me = General::createObjFromDB($generalID, null, GeneralQueryMode::FullWithAccessLog); @@ -40,27 +42,29 @@ $lastRefresh = $db->queryFirstField( ); $nextChange = $me->getAuxVar('next_change'); -if (!is_string($nextChange) || $nextChange === '') { +if (!is_int($nextChange)) { $nextChange = null; } +$nextChangeDisplay = $nextChange === null ? null : $clock->formatTick($nextChange); increaseRefresh("내정보", 1); if ($gameStor->turntime <= $gameStor->opentime) { $targetTime = $me->getAuxVar('prestart_delete_after'); - if (!is_string($targetTime) || $targetTime === '') { + if (!is_int($targetTime)) { $targetTime = addTurn( - $lastRefresh ?: TimeUtil::now(), + $lastRefresh === null ? $nowTick : Util::toInt($lastRefresh), $gameStor->turnterm, GameConst::$minTurnDieOnPrestart ); $me->setAuxVar('prestart_delete_after', $targetTime); $me->applyDB($db); } + $targetTimeDisplay = $clock->formatTick($targetTime); //서버 가오픈시 할 수 있는 행동 if ($me->getNPCType() == 0 && $me->getNationID() == 0) { $showDieOnPrestartBtn = true; - if ($targetTime <= TimeUtil::now()) { + if ($targetTime <= $nowTick) { $availableDieOnPrestart = true; } } @@ -173,7 +177,7 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $ --> - 가오픈 기간 내 장수 삭제 ( 부터)
+ 가오픈 기간 내 장수 삭제 ( 부터)


@@ -188,7 +192,7 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $ npcmode == 2 && $me->getNPCType() == 0) : ?> - 다른 장수 선택 (부터)
+ 다른 장수 선택 (부터)


diff --git a/hwe/c_tournament.php b/hwe/c_tournament.php index 9538f778..2c9f8f0e 100644 --- a/hwe/c_tournament.php +++ b/hwe/c_tournament.php @@ -228,10 +228,10 @@ if($btn == "자동개최설정") { $phase = 0; startBetting($admin['tnmt_type'], 720); } elseif($btn == "베팅마감") { - $dt = date("Y-m-d H:i:s", time() + 60); + $clock = GameClock::fromStorage($gameStor); $gameStor->tournament=7; $gameStor->phase=0; - $gameStor->tnmt_time = $dt; + $gameStor->tnmt_time = $clock->nowTick() + $clock->ticksFromSeconds(60); } elseif($btn == "16강") { finalFight($admin['tnmt_type'], $admin['tournament'], $admin['phase'], 16); } elseif($btn == "8강") { @@ -248,4 +248,4 @@ if($btn == "자동개최설정") { $gameStor->tnmt_msg = $msg; } -header('location:b_tournament.php'); \ No newline at end of file +header('location:b_tournament.php'); diff --git a/hwe/func.php b/hwe/func.php index 026c4b69..d72cad9b 100644 --- a/hwe/func.php +++ b/hwe/func.php @@ -630,7 +630,11 @@ function generalInfo(General $generalObj) $injury = "건강"; } - $remaining = (new \DateTimeImmutable($generalObj->getTurnTime()))->diff(new \DateTimeImmutable())->i; + $clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); + $remaining = max(0, intdiv( + $generalObj->getTurnTick() - $clock->nowTick(), + $clock->ticksFromMinutes(1), + )); if ($nation['color'] == "") { $nation['color'] = "#000000"; @@ -921,45 +925,37 @@ function banner() ); } -function addTurn($date, int $turnterm, int $turn = 1, bool $withFraction = true) -{ - $date = new \DateTime($date); - $target = $turnterm * $turn; - $date->add(new \DateInterval("PT{$target}M")); - if ($withFraction) { - return $date->format('Y-m-d H:i:s.u'); - } - return $date->format('Y-m-d H:i:s'); -} - -function subTurn($date, int $turnterm, int $turn = 1, bool $withFraction = true) -{ - $date = new \DateTime($date); - $target = $turnterm * $turn; - $date->sub(new \DateInterval("PT{$target}M")); - if ($withFraction) { - return $date->format('Y-m-d H:i:s.u'); - } - return $date->format('Y-m-d H:i:s'); -} - -function cutTurn($date, int $turnterm, bool $withFraction = true) -{ - $date = new \DateTime($date); - - $baseDate = new \DateTime($date->format('Y-m-d')); - $baseDate->sub(new \DateInterval("P1D")); - $baseDate->add(new \DateInterval("PT1H")); - - $diffMin = intdiv($date->getTimeStamp() - $baseDate->getTimeStamp(), 60); - $diffMin -= $diffMin % $turnterm; - - $baseDate->add(new \DateInterval("PT{$diffMin}M")); - if ($withFraction) { - return $baseDate->format('Y-m-d H:i:s.u'); - } - return $baseDate->format('Y-m-d H:i:s'); -} +function addTurn(int $tick, int $turnterm, int $turn = 1, bool $withFraction = true): int +{ + return $tick + GameClock::TICKS_PER_TURN * $turn; +} + +function subTurn(int $tick, int $turnterm, int $turn = 1, bool $withFraction = true): int +{ + return $tick - GameClock::TICKS_PER_TURN * $turn; +} + +function cutTurn(int $tick, int $turnterm, bool $withFraction = true): int +{ + $remainder = $tick % GameClock::TICKS_PER_TURN; + if ($remainder < 0) { + $remainder += GameClock::TICKS_PER_TURN; + } + return $tick - $remainder; +} + +/** 시나리오 초기화 입력인 벽시계를 기존 01:00 기준 월 경계로 정렬합니다. */ +function cutTurnDateTime(string $date, int $turnterm, bool $withFraction = true): string +{ + $dateObj = new \DateTime($date); + $baseDate = new \DateTime($dateObj->format('Y-m-d')); + $baseDate->sub(new \DateInterval('P1D')); + $baseDate->add(new \DateInterval('PT1H')); + $diffMin = intdiv($dateObj->getTimestamp() - $baseDate->getTimestamp(), 60); + $diffMin -= $diffMin % $turnterm; + $baseDate->add(new \DateInterval("PT{$diffMin}M")); + return $baseDate->format($withFraction ? 'Y-m-d H:i:s.u' : 'Y-m-d H:i:s'); +} function cutDay($date, int $turnterm, bool $withFraction = true) { @@ -1001,11 +997,9 @@ function increaseRefresh($type = "", $cnt = 1) $generalID = $session->generalID; $userGrade = $session->userGrade; - $dateObj = new \DateTimeImmutable(); - $date = TimeUtil::format($dateObj, false); - - $db = DB::db(); - $gameStor = KVStorage::getStorage($db, 'game_env'); + $db = DB::db(); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $date = GameClock::fromStorage($gameStor)->nowTick(); $isunited = $gameStor->isunited; $opentime = $gameStor->opentime; @@ -1145,18 +1139,19 @@ function unlock(): bool return $db->affectedRows() > 0; } -function timeover(): bool +function timeover(): bool { $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); - list($turnterm, $turntime) = $gameStor->getValuesAsArray(['turnterm', 'turntime']); - $diff = (new \DateTime())->getTimestamp() - (new \DateTime($turntime))->getTimestamp(); + list($turnterm, $turntime) = $gameStor->getValuesAsArray(['turnterm', 'turntime']); + $clock = GameClock::fromStorage($gameStor); + $diff = $clock->nowTick() - Util::toInt($turntime); $t = min($turnterm, 5); - $term = $diff; - if ($term >= $t || $term < 0) { + $term = $clock->ticksFromSeconds($t); + if ($diff >= $term || $diff < 0) { return true; } else { return false; @@ -1169,9 +1164,8 @@ function checkDelay() $gameStor = KVStorage::getStorage($db, 'game_env'); //서버정보 - $now = new \DateTimeImmutable(); - $turntime = new \DateTimeImmutable($gameStor->turntime); - $timeMinDiff = intdiv($now->getTimestamp() - $turntime->getTimestamp(), 60); + $clock = GameClock::fromStorage($gameStor); + $timeMinDiff = intdiv($clock->nowTick() - Util::toInt($gameStor->turntime), $clock->ticksFromMinutes(1)); // 1턴이상 갱신 없었으면 서버 지연 $term = $gameStor->turnterm; @@ -1185,20 +1179,17 @@ function checkDelay() //지연 해야할 밀린 턴 횟수 $iter = intdiv($timeMinDiff, $term); if ($iter > $threshold) { - $minute = $iter * $term; - $newTurntime = $turntime->add(new \DateInterval("PT{$minute}M")); - $newNextTurntime = $turntime->add(new \DateInterval("PT{$term}M")); - $gameStor->turntime = $newTurntime->format('Y-m-d H:i:s'); - $gameStor->starttime = (new \DateTimeImmutable($gameStor->starttime)) - ->add(new \DateInterval("PT{$minute}M")) - ->format('Y-m-d H:i:s'); - - $db->update('general', [ - 'turntime' => $db->sqleval('DATE_ADD(turntime, INTERVAL %i MINUTE)', $minute) - ], 'turntime<=DATE_ADD(turntime, INTERVAL %i MINUTE)', $term); - $db->update('ng_auction', [ - 'close_date' => $db->sqleval('DATE_ADD(close_date, INTERVAL %i MINUTE)', $minute) - ], 'finished = 0'); + $minute = $iter * $term; + $delayTick = $clock->ticksFromMinutes($minute); + $gameStor->turntime = Util::toInt($gameStor->turntime) + $delayTick; + $gameStor->starttime = Util::toInt($gameStor->starttime) + $delayTick; + + $db->update('general', [ + 'turntime' => $db->sqleval('turntime + %i', $delayTick) + ], true); + $db->update('ng_auction', [ + 'close_tick' => $db->sqleval('close_tick + %i', $delayTick) + ], 'finished = 0'); } } @@ -1247,17 +1238,15 @@ function updateOnline() $gameStor->online_nation = join(', ', $onlineNation); } -function turnDate($curtime) +function turnDate($curtime) { $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); $admin = $gameStor->getValues(['startyear', 'starttime', 'turnterm', 'year', 'month']); $turn = $admin['starttime']; - $curturn = cutTurn($curtime, $admin['turnterm']); - $term = $admin['turnterm']; - - $num = intdiv((strtotime($curturn) - strtotime($turn)), $term * 60); + $curturn = cutTurn(Util::toInt($curtime), $admin['turnterm']); + $num = intdiv($curturn - Util::toInt($turn), GameClock::TICKS_PER_TURN); $date = $admin['startyear'] * 12; $date += $num; @@ -1705,12 +1694,21 @@ function tryUniqueItemLottery(RandUtil $rng, General $general, string $acquireTy return giveRandomUniqueItem($rng, $general, $acquireType); } -function getAdmin() -{ - $db = DB::db(); - $gameStor = KVStorage::getStorage($db, 'game_env'); - return $gameStor->getAll(); -} +function getAdmin() +{ + $db = DB::db(); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $admin = $gameStor->getAll(); + $clock = GameClock::fromStorage($gameStor); + foreach (['turntime', 'starttime', 'opentime', 'tnmt_time'] as $key) { + if (($admin[$key] ?? null) !== null) { + $admin["{$key}_display"] = $clock->formatTick(Util::toInt($admin[$key]), true); + } + } + $admin['clock_now_tick'] = $clock->nowTick(); + $admin['clock_now_display'] = $clock->formatTick($admin['clock_now_tick'], true); + return $admin; +} /** @return General[] */ function deleteNation(General $lord, bool $applyDB): array @@ -2199,35 +2197,29 @@ function SabotageInjury(RandUtil $rng, array $cityGeneralList, string $reason): return $injuryCount; } -function getRandTurn(RandUtil $rng, $term, ?\DateTimeInterface $baseDateTime = null) -{ - if ($baseDateTime === null) { - $baseDateTime = new \DateTimeImmutable(); - } else if ($baseDateTime instanceof \DateTime) { - $baseDateTime = \DateTimeImmutable::createFromMutable($baseDateTime); - } else if ($baseDateTime instanceof \DateTimeImmutable) { - //do Nothing - } else { - throw new MustNotBeReachedException(); - } - - $randSecond = $rng->nextRangeInt(0, 60 * $term - 1); - $randFraction = $rng->nextRangeInt(0, 999999) / 1000000; //6자리 소수 - - return TimeUtil::format($baseDateTime->add(TimeUtil::secondsToDateInterval($randSecond + $randFraction)), true); -} - -function getRandTurn2(RandUtil $rng, $term, ?\DateTimeInterface $baseDateTime = null) -{ - if ($baseDateTime === null) { - $baseDateTime = new \DateTimeImmutable(); - } else if ($baseDateTime instanceof \DateTime) { - $baseDateTime = \DateTimeImmutable::createFromMutable($baseDateTime); - } else { - throw new MustNotBeReachedException(); - } - $randSecond = $rng->nextRangeInt(0, 60 * $term - 1); - $randFraction = $rng->nextRangeInt(0, 999999) / 1000000; //6자리 소수 - - return $baseDateTime->sub(TimeUtil::secondsToDateInterval($randSecond + $randFraction))->format('Y-m-d H:i:s.u'); -} +function getRandTurn(RandUtil $rng, int $term, ?int $baseTick = null): int +{ + $db = DB::db(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); + $baseTick ??= $clock->nowTick(); + + $randSecond = $rng->nextRangeInt(0, 60 * $term - 1); + $randMicrosecond = $rng->nextRangeInt(0, 999999); // 레거시 RNG 소비 6자리 유지 + + return $baseTick + + $clock->ticksFromSeconds($randSecond) + + intdiv($randMicrosecond * $clock->ticksPerSecond(), 1_000_000); +} + +function getRandTurn2(RandUtil $rng, int $term, ?int $baseTick = null): int +{ + $db = DB::db(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); + $baseTick ??= $clock->nowTick(); + $randSecond = $rng->nextRangeInt(0, 60 * $term - 1); + $randMicrosecond = $rng->nextRangeInt(0, 999999); // 레거시 RNG 소비 6자리 유지 + + return $baseTick + - $clock->ticksFromSeconds($randSecond) + - intdiv($randMicrosecond * $clock->ticksPerSecond(), 1_000_000); +} diff --git a/hwe/func_auction.php b/hwe/func_auction.php index c5196952..8064122c 100644 --- a/hwe/func_auction.php +++ b/hwe/func_auction.php @@ -62,11 +62,12 @@ function processAuction() { $db = DB::db(); - $now = TimeUtil::now(); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $nowTick = GameClock::fromStorage($gameStor)->nowTick(); $auctionList = $db->queryAllLists( - 'SELECT id, `type` FROM ng_auction WHERE `close_date` <= %s AND finished = 0', - $now + 'SELECT id, `type` FROM ng_auction WHERE `close_tick` <= %i AND finished = 0', + $nowTick ); if (!$auctionList) { diff --git a/hwe/func_gamerule.php b/hwe/func_gamerule.php index 56883c7f..707d3406 100644 --- a/hwe/func_gamerule.php +++ b/hwe/func_gamerule.php @@ -651,16 +651,24 @@ function checkStatistic() } -function convForOldGeneral(array $general, int $year, int $month) -{ - $general['history'] = getGeneralHistoryLogAll($general['no']); - return [ +function convForOldGeneral(array $general, int $year, int $month) +{ + $general['history'] = getGeneralHistoryLogAll($general['no']); + $clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); + $turnTimeDisplay = $clock->formatTick(Util::toInt($general['turntime']), true); + $general['turntime_tick'] = Util::toInt($general['turntime']); + $general['turntime'] = $turnTimeDisplay; + if ($general['recent_war'] !== null) { + $general['recent_war_tick'] = Util::toInt($general['recent_war']); + $general['recent_war'] = $clock->formatTick(Util::toInt($general['recent_war']), true); + } + return [ 'server_id' => UniqueConst::$serverID, 'general_no' => $general['no'], 'owner' => $general['owner'], 'name' => $general['name'], 'last_yearmonth' => $year * 100 + $month, - 'turntime' => $general['turntime'], + 'turntime' => $turnTimeDisplay, 'data' => Json::encode($general) ]; } @@ -734,7 +742,7 @@ function checkEmperior() /** @var int[] */ $auctionList = $db->queryFirstColumn( - 'SELECT `id` FROM `ng_auction` WHERE `type` = %s AND `finished` = 0 ORDER BY `close_date` ASC', + 'SELECT `id` FROM `ng_auction` WHERE `type` = %s AND `finished` = 0 ORDER BY `close_tick` ASC', AuctionType::UniqueItem->value ); foreach($auctionList as $auctionID){ diff --git a/hwe/func_legacy.php b/hwe/func_legacy.php index deaf55c3..d7f5ff61 100644 --- a/hwe/func_legacy.php +++ b/hwe/func_legacy.php @@ -4,6 +4,10 @@ namespace sammo; function printLimitMsg($turntime) { + if (is_int($turntime) || (is_string($turntime) && ctype_digit(ltrim($turntime, '-')))) { + $gameStor = KVStorage::getStorage(DB::db(), 'game_env'); + $turntime = GameClock::fromStorage($gameStor)->formatTick(Util::toInt($turntime), true); + } //FIXME: template로 이동. ?> diff --git a/hwe/func_template.php b/hwe/func_template.php index dcb3dacb..b4e02b58 100644 --- a/hwe/func_template.php +++ b/hwe/func_template.php @@ -61,8 +61,12 @@ function chiefTurnTable() "; } -function templateLimitMsg(string $turntime): string -{ +function templateLimitMsg(string $turntime): string +{ + if (ctype_digit(ltrim($turntime, '-'))) { + $gameStor = KVStorage::getStorage(DB::db(), 'game_env'); + $turntime = GameClock::fromStorage($gameStor)->formatTick(Util::toInt($turntime), true); + } return "이미 너무 많은 접속을 하셨습니다. 다음 턴에 다시 시도해주세요. (턴시간: {$turntime})"; } diff --git a/hwe/func_tournament.php b/hwe/func_tournament.php index c8046516..4de5c973 100644 --- a/hwe/func_tournament.php +++ b/hwe/func_tournament.php @@ -17,10 +17,10 @@ function processTournament() { $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); $admin = $gameStor->getValues(['tournament', 'phase', 'tnmt_type', 'tnmt_auto', 'tnmt_time', 'turnterm', 'last_tournament_betting_id']); - $now = new \DateTime(); - $offset = $now->getTimestamp() - (new \DateTime($admin['tnmt_time']))->getTimestamp(); + $offset = intdiv($clock->nowTick() - Util::toInt($admin['tnmt_time']), $clock->ticksPerSecond()); //수동일땐 무시 if (!$admin['tnmt_auto']) { @@ -122,10 +122,10 @@ function processTournament() if ($tnmt == 6) { $betTerm = Util::valueFit($unit * 60, null, 3600); //처리 초 더한 날짜 - $dt = date("Y-m-d H:i:s", strtotime($admin['tnmt_time']) + $unit * $i + $betTerm); $gameStor->tournament = $tnmt; $gameStor->phase = $phase; - $gameStor->tnmt_time = $dt; + $gameStor->tnmt_time = Util::toInt($admin['tnmt_time']) + + $clock->ticksFromSeconds($unit * $i + $betTerm); return; } } @@ -133,7 +133,7 @@ function processTournament() $second = $unit * $iter; $gameStor->tournament = $tnmt; $gameStor->phase = $phase; - $gameStor->tnmt_time = (new \DateTimeImmutable($admin['tnmt_time']))->add(new \DateInterval("PT{$second}S"))->format('Y-m-d H:i:s'); + $gameStor->tnmt_time = Util::toInt($admin['tnmt_time']) + $clock->ticksFromSeconds($second); } function getTournamentTermText(int $turnTerm) @@ -160,7 +160,8 @@ function getTournamentTime() $gameStor = KVStorage::getStorage($db, 'game_env'); list($tnmt, $tnmt_time) = $gameStor->getValuesAsArray(['tournament', 'tnmt_time']); - $dt = substr($tnmt_time, 11, 5); + $clock = GameClock::fromStorage($gameStor); + $dt = substr($clock->formatTick(Util::toInt($tnmt_time)), 11, 5); switch ($tnmt) { case 1: $tnmt = "개막시간 {$dt}"; @@ -284,9 +285,11 @@ function startTournament($type) $admin = $gameStor->getValues(['year', 'month', 'turnterm']); $turnTerm = $admin['turnterm']; $unit = calcTournamentTerm($turnTerm); + $clock = GameClock::fromStorage($gameStor); $gameStor->tnmt_auto = true; - $gameStor->tnmt_time = (new \DateTimeImmutable())->add(new \DateInterval("PT{$unit}M"))->format('Y-m-d H:i:s'); + // 기존 startTournament은 unit을 분으로 더하므로 그 계약을 유지합니다. + $gameStor->tnmt_time = $clock->nowTick() + $clock->ticksFromMinutes($unit); $gameStor->tournament = 1; $gameStor->tnmt_type = $type; $gameStor->last_tournament_betting_id = 0; @@ -1171,7 +1174,7 @@ function fight($tnmt_type, $tnmt, $phs, $group, $g1, $g2, $type) } $damage1 *= $factor1; $damage2 *= $factor2; - + //1합 승부 if ($phase == 1) { diff --git a/hwe/j_get_select_npc_token.php b/hwe/j_get_select_npc_token.php index 0227114d..ccbcd081 100644 --- a/hwe/j_get_select_npc_token.php +++ b/hwe/j_get_select_npc_token.php @@ -16,12 +16,10 @@ $session = Session::requireLogin([])->setReadOnly(); $userID = Session::getUserID(); -$oNow = new \DateTimeImmutable(); - -$now = $oNow->format('Y-m-d H:i:s'); - $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); +$clock = GameClock::fromStorage($gameStor); +$now = $clock->nowTick(); $oldGeneral = $db->queryFirstField('SELECT `no` FROM general WHERE `owner`=%i', $userID); if($oldGeneral !== null){ @@ -44,15 +42,14 @@ if($npcmode!=1){ ]); } -$token = $db->queryFirstRow('SELECT * FROM select_npc_token WHERE `owner`=%i AND `valid_until`>=%s', $userID, $now); +$token = $db->queryFirstRow('SELECT * FROM select_npc_token WHERE `owner`=%i AND `valid_until`>=%i', $userID, $now); $pickResult = []; if($token && $refresh){ - $pickMoreFrom = (new \DateTime($token['pick_more_from']))->getTimestamp(); - $nowT = $oNow->getTimestamp(); + $pickMoreFrom = Util::toInt($token['pick_more_from']); - if($nowT >= $pickMoreFrom){ + if($now >= $pickMoreFrom){ $oldPickResult = Json::decode($token['pick_result']); foreach($keepResult as $keepId){ @@ -75,15 +72,14 @@ if($token && $refresh){ } if($token && !$refresh){ - $pickMoreFrom = (new \DateTime($token['pick_more_from']))->getTimestamp(); - $nowT = $oNow->getTimestamp(); + $pickMoreFrom = Util::toInt($token['pick_more_from']); Json::die([ 'result'=>true, 'pick'=>Json::decode($token['pick_result']), - 'pickMoreFrom'=>$token['pick_more_from'], - 'pickMoreSeconds'=>$pickMoreFrom-$nowT, - 'validUntil'=>$token['valid_until'] + 'pickMoreFrom'=>$clock->formatTick($pickMoreFrom), + 'pickMoreSeconds'=>intdiv($pickMoreFrom - $now, $clock->ticksPerSecond()), + 'validUntil'=>$clock->formatTick(Util::toInt($token['valid_until'])) ]); } @@ -100,7 +96,7 @@ foreach($db->query('SELECT `no`, `name`, leadership, strength, intel, nation, im $weight[$general['no']] = pow($allStat, 1.5); } -foreach($db->queryFirstColumn('SELECT pick_result FROM select_npc_token WHERE `owner`!=%i AND valid_until >=%s', $userID, $now) as $reserved){ +foreach($db->queryFirstColumn('SELECT pick_result FROM select_npc_token WHERE `owner`!=%i AND valid_until >=%i', $userID, $now) as $reserved){ $reserved = Json::decode($reserved); foreach(array_keys($reserved) as $reservedNPC){ if(key_exists($reservedNPC, $weight)){ @@ -131,8 +127,8 @@ $newNonce = random_int(0, 0xfffffff); $validSecond = max(VALID_SECOND, $turnterm*40); $pickMoreSecond = max(PICK_MORE_SECOND, Util::round(pow($turnterm, 0.672)*8)); -$validUntil = $oNow->add(new \DateInterval(sprintf('PT%dS', $validSecond))); -$pickMoreFrom = $oNow->add(new \DateInterval(sprintf('PT%dS', $pickMoreSecond))); +$validUntil = $now + $clock->ticksFromSeconds($validSecond); +$pickMoreFrom = $now + $clock->ticksFromSeconds($pickMoreSecond); $db->delete('select_npc_token', 'valid_until < %s', $now); @@ -140,8 +136,8 @@ $inserted = 0; if($token){ $db->update('select_npc_token', [ - 'valid_until'=>$validUntil->format('Y-m-d H:i:s'), - 'pick_more_from'=>$pickMoreFrom->format('Y-m-d H:i:s'), + 'valid_until'=>$validUntil, + 'pick_more_from'=>$pickMoreFrom, 'pick_result'=>Json::encode($pickResult), 'nonce'=>$newNonce ], 'owner = %i AND nonce = %i', $userID, $token['nonce']); @@ -152,8 +148,8 @@ if($token){ else{ $db->insertIgnore('select_npc_token', [ 'owner'=>$userID, - 'valid_until'=>$validUntil->format('Y-m-d H:i:s'), - 'pick_more_from'=>'2000-01-01 01:00:00', + 'valid_until'=>$validUntil, + 'pick_more_from'=>$now, 'pick_result'=>Json::encode($pickResult), 'nonce'=>$newNonce ]); @@ -173,7 +169,7 @@ if($inserted === 0){ Json::die([ 'result'=>true, 'pick'=>$pickResult, - 'pickMoreFrom'=>($inserted===-1)?$pickMoreFrom->format('Y-m-d H:i:s'):'2000-01-01 01:00:00', + 'pickMoreFrom'=>$clock->formatTick(($inserted===-1)?$pickMoreFrom:$now), 'pickMoreSeconds'=>($inserted===-1)?$pickMoreSecond:0, - 'validUntil'=>$validUntil->format('Y-m-d H:i:s') -]); \ No newline at end of file + 'validUntil'=>$clock->formatTick($validUntil) +]); diff --git a/hwe/j_get_select_pool.php b/hwe/j_get_select_pool.php index d5c9a07e..9e3d9c0b 100644 --- a/hwe/j_get_select_pool.php +++ b/hwe/j_get_select_pool.php @@ -43,12 +43,10 @@ function putInfoText(&$info, ?array $currentTargetEnv){ $session = Session::requireLogin([])->setReadOnly(); $userID = Session::getUserID(); -$oNow = new \DateTimeImmutable(); - -$now = $oNow->format('Y-m-d H:i:s'); - -$db = DB::db(); -$gameStor = KVStorage::getStorage($db, 'game_env'); +$db = DB::db(); +$gameStor = KVStorage::getStorage($db, 'game_env'); +$clock = GameClock::fromStorage($gameStor); +$now = $clock->nowTick(); $eventEnv = $gameStor->getValues(['npcmode', 'startyear', 'year', 'month']); $npcmode = $eventEnv['npcmode']; @@ -88,7 +86,7 @@ if($tokens){ Json::die([ 'result'=>true, 'pick'=>$pick, - 'validUntil'=>$valid_until + 'validUntil'=>$clock->formatTick(Util::toInt($valid_until)) ]); } @@ -108,5 +106,5 @@ sortTokens($pick);//좀 무식하지만.. Json::die([ 'result'=>true, 'pick'=>$pick, - 'validUntil'=>$valid_until + 'validUntil'=>$valid_until === null ? null : $clock->formatTick(Util::toInt($valid_until)) ]); diff --git a/hwe/j_install.php b/hwe/j_install.php index 6aa8f724..ef6dbbdd 100644 --- a/hwe/j_install.php +++ b/hwe/j_install.php @@ -211,7 +211,7 @@ try{ $show_img_level, !!$tournament_trig, $join_mode, - TimeUtil::now(), + TimeUtil::format(GameClock::readWallTime(), false), $autorun_user )); } @@ -220,4 +220,4 @@ catch(\Exception $e){ 'result'=>false, 'reason'=>$e->getMessage() ]); -} \ No newline at end of file +} diff --git a/hwe/j_select_npc.php b/hwe/j_select_npc.php index 61cbcc01..261fa46f 100644 --- a/hwe/j_select_npc.php +++ b/hwe/j_select_npc.php @@ -24,8 +24,8 @@ $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); $rootDB = RootDB::db(); -$oNow = new \DateTimeImmutable(); -$now = $oNow->format('Y-m-d H:i:s'); +$clock = GameClock::fromStorage($gameStor); +$now = $clock->nowTick(); $member = $rootDB->queryFirstRow('SELECT `name`, `penalty` FROM member WHERE `NO`=%i', $userID); if(!$member){ @@ -139,4 +139,4 @@ $rootDB->insert('member_log', [ Json::die([ 'result'=>true, 'reason'=>'success' -]); \ No newline at end of file +]); diff --git a/hwe/j_select_picked_general.php b/hwe/j_select_picked_general.php index 0b014069..f64d440b 100644 --- a/hwe/j_select_picked_general.php +++ b/hwe/j_select_picked_general.php @@ -37,8 +37,8 @@ $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); $rootDB = RootDB::db(); -$oNow = new \DateTimeImmutable(); -$now = $oNow->format('Y-m-d H:i:s'); +$clock = GameClock::fromStorage($gameStor); +$now = $clock->nowTick(); $hasGeneralID = $db->queryFirstField('SELECT no FROM general WHERE owner = %i', $userID); if($hasGeneralID){ @@ -176,7 +176,7 @@ $builder->setOwner($userID); $builder->setOwnerName($userNick); $builder->setKillturn(5); $builder->setNPCType(0); -$builder->setAuxVar('next_change', TimeUtil::nowAddMinutes(12 * $env['turnterm'])); +$builder->setAuxVar('next_change', $now + GameClock::TICKS_PER_TURN * 12); $builder->setAuxVar( 'prestart_delete_after', addTurn($now, $env['turnterm'], GameConst::$minTurnDieOnPrestart) diff --git a/hwe/j_server_basic_info.php b/hwe/j_server_basic_info.php index 47b95a88..d7868a56 100644 --- a/hwe/j_server_basic_info.php +++ b/hwe/j_server_basic_info.php @@ -19,6 +19,7 @@ if(!class_exists('\\sammo\\DB')){ $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); +$clock = GameClock::fromStorage($gameStor); if(file_exists(__DIR__.'/.htaccess')){ $reserved = $db->queryFirstRow( @@ -75,8 +76,8 @@ $admin['maxUserCnt'] = $admin['maxgeneral']; $admin['npcMode'] = $admin['npcmode']; $admin['turnTerm'] = $admin['turnterm']; $admin['isUnited'] = $admin['isunited']; -$admin['starttime'] = substr($admin['opentime'], 5, 11); -$admin['turntime'] = substr($admin['turntime'], 5, 11); +$admin['starttime'] = substr($clock->formatTick(Util::toInt($admin['opentime'])), 5, 11); +$admin['turntime'] = substr($clock->formatTick(Util::toInt($admin['turntime'])), 5, 11); unset($admin['npcmode']); unset($admin['maxgeneral']); unset($admin['turnterm']); @@ -131,4 +132,4 @@ if($general){ Json::die([ 'game'=>$admin, 'me'=>$me?:null -]); \ No newline at end of file +]); diff --git a/hwe/j_simulate_battle.php b/hwe/j_simulate_battle.php index a43d1ee1..94d6cee8 100644 --- a/hwe/j_simulate_battle.php +++ b/hwe/j_simulate_battle.php @@ -77,7 +77,8 @@ $month = $query['month']; $repeatCnt = $query['repeatCnt']; $rawAttacker = $query['attackerGeneral']; -$rawAttacker['turntime'] = TimeUtil::now(); +$battleClock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); +$rawAttacker['turntime'] = $battleClock->formatTick($battleClock->nowTick()); $rawAttackerCity = $query['attackerCity']; $rawAttackerNation = $query['attackerNation']; diff --git a/hwe/j_update_picked_general.php b/hwe/j_update_picked_general.php index dc6d1ece..9cad78eb 100644 --- a/hwe/j_update_picked_general.php +++ b/hwe/j_update_picked_general.php @@ -22,8 +22,8 @@ $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); $rootDB = RootDB::db(); -$oNow = new \DateTimeImmutable(); -$now = $oNow->format('Y-m-d H:i:s'); +$clock = GameClock::fromStorage($gameStor); +$now = $clock->nowTick(); $generalID = $db->queryFirstField('SELECT no FROM general WHERE owner = %i', $userID); if(!$generalID){ @@ -181,7 +181,7 @@ if ($isCentennialAllStar) { if(key_exists('generalName', $info)){ $generalObj->updateVar('name', $info['generalName']); } -$generalObj->setAuxVar('next_change', TimeUtil::nowAddMinutes(12 * $turnterm)); +$generalObj->setAuxVar('next_change', $now + GameClock::TICKS_PER_TURN * 12); $userNick = $ownerInfo['name']; $generalObj->setVar('owner_name', $userNick); diff --git a/hwe/proc.php b/hwe/proc.php index 5e1e2ac7..c4e92cba 100644 --- a/hwe/proc.php +++ b/hwe/proc.php @@ -11,9 +11,11 @@ $db = DB::db(); $updated = false; $locked = false; $lastExecuted = TurnExecutionHelper::executeAllCommand($updated, $locked); +$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); Json::die([ 'result' => true, 'updated' => $updated, 'locked' => $locked, - 'lastExecuted' => $lastExecuted, -]); \ No newline at end of file + 'lastExecutedTick' => $lastExecuted, + 'lastExecuted' => $clock->formatTick($lastExecuted, true), +]); diff --git a/hwe/sammo/API/Auction/BidUniqueAuction.php b/hwe/sammo/API/Auction/BidUniqueAuction.php index d026ad3a..6877c09c 100644 --- a/hwe/sammo/API/Auction/BidUniqueAuction.php +++ b/hwe/sammo/API/Auction/BidUniqueAuction.php @@ -21,7 +21,7 @@ class BidUniqueAuction extends \sammo\BaseAPI ]) ->rule('int', 'amount') ->rule('int', 'auctionID') - ->rule('boolean', 'extendCloseDate'); + ->rule('boolean', 'extendCloseTick'); if (!$v->validate()) { return $v->errorStr(); @@ -38,7 +38,7 @@ class BidUniqueAuction extends \sammo\BaseAPI { $auctionID = $this->args['auctionID']; $amount = $this->args['amount']; - $tryExtendCloseDate = $this->args['extendCloseDate'] ?? false; + $tryExtendCloseDate = $this->args['extendCloseTick'] ?? false; $generalID = $session->generalID; $general = General::createObjFromDB($generalID); diff --git a/hwe/sammo/API/Auction/GetActiveResourceAuctionList.php b/hwe/sammo/API/Auction/GetActiveResourceAuctionList.php index f891d00e..4e03e87a 100644 --- a/hwe/sammo/API/Auction/GetActiveResourceAuctionList.php +++ b/hwe/sammo/API/Auction/GetActiveResourceAuctionList.php @@ -15,6 +15,8 @@ use sammo\General; use sammo\Json; use sammo\TimeUtil; use sammo\Util; +use sammo\GameClock; +use sammo\KVStorage; use function sammo\getAuctionLogRecent; @@ -33,12 +35,13 @@ class GetActiveResourceAuctionList extends \sammo\BaseAPI public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType { $db = DB::db(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); $buyRiceList = []; $sellRiceList = []; /** @var AuctionInfo[] */ $auctions = array_map(fn ($raw) => AuctionInfo::fromArray($raw), $db->query( - 'SELECT * FROM `ng_auction` WHERE `type` IN %ls AND `finished` = 0 ORDER BY `close_date` ASC', + 'SELECT * FROM `ng_auction` WHERE `type` IN %ls AND `finished` = 0 ORDER BY `close_tick` ASC', [ AuctionType::BuyRice->value, AuctionType::SellRice->value, @@ -87,8 +90,8 @@ class GetActiveResourceAuctionList extends \sammo\BaseAPI 'type' => $auction->type->value, 'hostGeneralID' => $auction->hostGeneralID, 'hostName' => $auction->detail->hostName, - 'openDate' => TimeUtil::format($auction->openDate, false), - 'closeDate' => TimeUtil::format($auction->closeDate, false), + 'openDate' => $clock->formatTick($auction->openTick), + 'closeDate' => $clock->formatTick($auction->closeTick), 'amount' => $auction->detail->amount, 'startBidAmount' => $auction->detail->startBidAmount, 'finishBidAmount' => $auction->detail->finishBidAmount, diff --git a/hwe/sammo/API/Auction/GetUniqueItemAuctionDetail.php b/hwe/sammo/API/Auction/GetUniqueItemAuctionDetail.php index 782995f3..d5bc702a 100644 --- a/hwe/sammo/API/Auction/GetUniqueItemAuctionDetail.php +++ b/hwe/sammo/API/Auction/GetUniqueItemAuctionDetail.php @@ -14,6 +14,8 @@ use sammo\Enums\GeneralQueryMode; use sammo\Enums\InheritanceKey; use sammo\InheritancePointManager; use sammo\TimeUtil; +use sammo\GameClock; +use sammo\KVStorage; use sammo\Validator; use sammo\General; @@ -42,6 +44,7 @@ class GetUniqueItemAuctionDetail extends \sammo\BaseAPI public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType { $db = DB::db(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); $generalID = $session->generalID; $auctionID = $this->args['auctionID']; @@ -92,9 +95,11 @@ class GetUniqueItemAuctionDetail extends \sammo\BaseAPI 'target' => $auction->target, 'isCallerHost' => $auction->hostGeneralID === $generalID, 'hostName' => $auction->detail->hostName, - 'closeDate' => TimeUtil::format($auction->closeDate, false), + 'closeDate' => $clock->formatTick($auction->closeTick), 'remainCloseDateExtensionCnt' => $auction->detail->remainCloseDateExtensionCnt, - 'availableLatestBidCloseDate' => TimeUtil::format($auction->detail->availableLatestBidCloseDate, false), + 'availableLatestBidCloseDate' => $auction->detail->availableLatestBidCloseTick === null + ? null + : $clock->formatTick($auction->detail->availableLatestBidCloseTick), ], 'bidList' => $responseBid, 'obfuscatedName' => $obfuscatedName, diff --git a/hwe/sammo/API/Auction/GetUniqueItemAuctionList.php b/hwe/sammo/API/Auction/GetUniqueItemAuctionList.php index d7ff0704..c69b7230 100644 --- a/hwe/sammo/API/Auction/GetUniqueItemAuctionList.php +++ b/hwe/sammo/API/Auction/GetUniqueItemAuctionList.php @@ -12,6 +12,8 @@ use sammo\Enums\APIRecoveryType; use sammo\Enums\AuctionType; use sammo\TimeUtil; use sammo\Util; +use sammo\GameClock; +use sammo\KVStorage; class GetUniqueItemAuctionList extends \sammo\BaseAPI { @@ -28,12 +30,13 @@ class GetUniqueItemAuctionList extends \sammo\BaseAPI public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType { $db = DB::db(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); $generalID = $session->generalID; /** @var AuctionInfo[] */ $auctions = array_map(fn($raw)=>AuctionInfo::fromArray($raw), $db->query( - 'SELECT * FROM `ng_auction` WHERE `type` = %s ORDER BY `close_date` ASC', + 'SELECT * FROM `ng_auction` WHERE `type` = %s ORDER BY `close_tick` ASC', AuctionType::UniqueItem->value ) ?? []); @@ -85,9 +88,11 @@ class GetUniqueItemAuctionList extends \sammo\BaseAPI 'target' => $auction->target, 'isCallerHost' => $auction->hostGeneralID === $generalID, 'hostName' => $auction->detail->hostName, - 'closeDate' => TimeUtil::format($auction->closeDate, false), + 'closeDate' => $clock->formatTick($auction->closeTick), 'remainCloseDateExtensionCnt' => $auction->detail->remainCloseDateExtensionCnt, - 'availableLatestBidCloseDate' => TimeUtil::format($auction->detail->availableLatestBidCloseDate, false), + 'availableLatestBidCloseDate' => $auction->detail->availableLatestBidCloseTick === null + ? null + : $clock->formatTick($auction->detail->availableLatestBidCloseTick), 'highestBid' => [ 'generalName' => $highestBid->aux->generalName, 'amount' => $highestBid->amount, diff --git a/hwe/sammo/API/Command/GetReservedCommand.php b/hwe/sammo/API/Command/GetReservedCommand.php index 8bec74a0..195376ed 100644 --- a/hwe/sammo/API/Command/GetReservedCommand.php +++ b/hwe/sammo/API/Command/GetReservedCommand.php @@ -7,9 +7,11 @@ use DateTimeInterface; use sammo\DB; use sammo\Enums\APIRecoveryType; use sammo\GameConst; +use sammo\GameClock; use sammo\Json; use sammo\KVStorage; use sammo\TimeUtil; +use sammo\Util; use function sammo\cutTurn; @@ -82,11 +84,12 @@ class GetReservedCommand extends \sammo\BaseAPI return [ 'result' => true, - 'turnTime' => $turnTime, + 'turnTimeTick' => Util::toInt($turnTime), + 'turnTime' => GameClock::fromStorage($gameStor)->formatTick(Util::toInt($turnTime)), 'turnTerm' => $turnTerm, 'year' => $year, 'month' => $month, - 'date' => TimeUtil::now(true), + 'date' => GameClock::fromStorage($gameStor)->formatTick(GameClock::fromStorage($gameStor)->nowTick(), true), 'turn' => $commandList, 'autorun_limit' => $generalAux['autorun_limit'] ?? null, ]; diff --git a/hwe/sammo/API/General/DieOnPrestart.php b/hwe/sammo/API/General/DieOnPrestart.php index c38a8673..1e20e86e 100644 --- a/hwe/sammo/API/General/DieOnPrestart.php +++ b/hwe/sammo/API/General/DieOnPrestart.php @@ -11,7 +11,8 @@ use sammo\Session; use sammo\General; use sammo\JosaUtil; use sammo\KVStorage; -use sammo\TimeUtil; +use sammo\GameClock; +use sammo\Util; use function sammo\addTurn; use function sammo\increaseRefresh; @@ -37,6 +38,8 @@ class DieOnPrestart extends \sammo\BaseAPI $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor->cacheValues(['turnterm', 'opentime', 'turntime', 'year', 'month']); + $clock = GameClock::fromStorage($gameStor); + $nowTick = $clock->nowTick(); $general = $db->queryFirstRow('SELECT no,name,nation,owner_name,npc FROM general WHERE owner=%i AND npc = 0', $userID); if (!$general) { @@ -67,9 +70,9 @@ class DieOnPrestart extends \sammo\BaseAPI } $targetTime = $generalObj->getAuxVar('prestart_delete_after'); - if (!is_string($targetTime) || $targetTime === '') { + if (!is_int($targetTime)) { $targetTime = addTurn( - $lastRefresh ?: TimeUtil::now(), + $lastRefresh === null ? $nowTick : Util::toInt($lastRefresh), $gameStor->turnterm, GameConst::$minTurnDieOnPrestart ); @@ -78,8 +81,8 @@ class DieOnPrestart extends \sammo\BaseAPI } //서버 가오픈시 할 수 있는 행동 - if ($targetTime > TimeUtil::now()) { - $targetTimeShort = substr($targetTime, 0, 19); + if ($targetTime > $nowTick) { + $targetTimeShort = $clock->formatTick($targetTime); return "아직 삭제할 수 없습니다. {$targetTimeShort} 부터 가능합니다."; } diff --git a/hwe/sammo/API/General/GetFrontInfo.php b/hwe/sammo/API/General/GetFrontInfo.php index c36cd570..8fc44ef7 100644 --- a/hwe/sammo/API/General/GetFrontInfo.php +++ b/hwe/sammo/API/General/GetFrontInfo.php @@ -14,6 +14,7 @@ use sammo\Enums\GeneralColumn; use sammo\Enums\GeneralQueryMode; use sammo\Enums\RankColumn; use sammo\GameConst; +use sammo\GameClock; use sammo\General; use sammo\KVStorage; use sammo\LastTurn; @@ -105,6 +106,7 @@ class GetFrontInfo extends \sammo\BaseAPI $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); $gameStor->cacheValues(['isunited', 'opentime', 'refresh']); $lastHistoryID = $this->args['lastWorldHistoryID']; @@ -158,6 +160,7 @@ class GetFrontInfo extends \sammo\BaseAPI private function generateGlobalInfo(MeekroDB $db): array { $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); [ $scenarioText, $extendedGeneral, $isFiction, $npcMode, @@ -210,7 +213,8 @@ class GetFrontInfo extends \sammo\BaseAPI 'month' => $month, 'autorunUser' => $autorunUser, 'turnterm' => $turnterm, - 'lastExecuted' => $lastExecuted, + 'lastExecutedTick' => $lastExecuted, + 'lastExecuted' => $clock->formatTick(Util::toInt($lastExecuted), true), 'lastVoteID' => $lastVoteID, 'develCost' => $develCost, 'noticeMsg' => $noticeMsg, @@ -224,7 +228,8 @@ class GetFrontInfo extends \sammo\BaseAPI 'isLocked' => $isLocked, 'tournamentType' => $tournamentType, 'tournamentState' => $tournamentState, - 'tournamentTime' => $tournamentTime, + 'tournamentTimeTick' => $tournamentTime, + 'tournamentTime' => $tournamentTime === null ? null : $clock->formatTick(Util::toInt($tournamentTime)), 'genCount' => $globalGenCount, 'generalCntLimit' => $generalCntLimit, 'serverCnt' => $serverCnt, @@ -361,6 +366,7 @@ class GetFrontInfo extends \sammo\BaseAPI public function generateGeneralInfo(MeekroDB $db, General $general, array $rawNation): array { + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); $permission = checkSecretPermission($general->getRaw()); @@ -425,8 +431,12 @@ class GetFrontInfo extends \sammo\BaseAPI 'crew' => $general->getVar(GeneralColumn::crew), // number; 'train' => $general->getVar(GeneralColumn::train), // number; 'atmos' => $general->getVar(GeneralColumn::atmos), // number; - 'turntime' => $general->getVar(GeneralColumn::turntime), // string; - 'recent_war' => $general->getVar(GeneralColumn::recent_war), // string; + 'turntimeTick' => $general->getTurnTick(), // number; + 'turntime' => $general->getTurnTime(), // string; + 'recent_war_tick' => $general->getVar(GeneralColumn::recent_war), // number|null; + 'recent_war' => $general->getVar(GeneralColumn::recent_war) === null + ? null + : $clock->formatTick(Util::toInt($general->getVar(GeneralColumn::recent_war)), true), // string|null; 'horse' => $general->getVar(GeneralColumn::horse), // GameObjClassKey; 'weapon' => $general->getVar(GeneralColumn::weapon), // GameObjClassKey; 'book' => $general->getVar(GeneralColumn::book), // GameObjClassKey; @@ -544,7 +554,7 @@ class GetFrontInfo extends \sammo\BaseAPI 'result' => false, 'reason' => '접속 제한중입니다.', 'recovery' => APIRecoveryType::GameQuota, - 'recovery_arg' => $general->getVar('turntime'), + 'recovery_arg' => $general->getTurnTime(), ]; } diff --git a/hwe/sammo/API/General/Join.php b/hwe/sammo/API/General/Join.php index 43b5a2cc..eae1a354 100644 --- a/hwe/sammo/API/General/Join.php +++ b/hwe/sammo/API/General/Join.php @@ -11,6 +11,7 @@ use sammo\Enums\APIRecoveryType; use sammo\Enums\GeneralAccessLogColumn; use sammo\Enums\RankColumn; use sammo\GameConst; +use sammo\GameClock; use sammo\GameUnitConst; use sammo\General; use sammo\InheritancePointManager; @@ -165,6 +166,7 @@ class Join extends \sammo\BaseAPI $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor->cacheValues(['year', 'month', 'maxgeneral', 'scenario', 'show_img_level', 'block_general_create', 'turnterm', 'turntime', 'genius', 'npcmode']); + $clock = GameClock::fromStorage($gameStor); ########## 동일 정보 존재여부 확인. ########## $block_general_create = $gameStor->getValue('block_general_create'); @@ -222,7 +224,7 @@ class Join extends \sammo\BaseAPI $userLogger = new UserLogger($userID, $admin['year'], $admin['month'], false); - $now = TimeUtil::now(false); + $now = $clock->nowTick(); $rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize( UniqueConst::$hiddenSeed, 'MakeGeneral', @@ -360,17 +362,14 @@ class Join extends \sammo\BaseAPI $userLogger->push(sprintf("턴 시간 %02d:%02d 로 지정", intdiv($inheritTurntime, 60), $inheritTurntime % 60), "inheritPoint"); - $inheritTurntime += $rng->nextRangeInt(0, 999999) / 1000000; - - $turntime = new \DateTimeImmutable(cutTurn($admin['turntime'], $admin['turnterm'])); - $turntime = $turntime->add(TimeUtil::secondsToDateInterval($inheritTurntime)); - $turntime = TimeUtil::format($turntime, true); + $inheritTurnMicrosecond = $rng->nextRangeInt(0, 999999); + $turntime = cutTurn(Util::toInt($admin['turntime']), $admin['turnterm']) + + $clock->ticksFromSeconds($inheritTurntime) + + intdiv($inheritTurnMicrosecond * $clock->ticksPerSecond(), 1_000_000); } else { - $turntime = getRandTurn($rng, $admin['turnterm'], new \DateTimeImmutable($admin['turntime'])); + $turntime = getRandTurn($rng, $admin['turnterm'], Util::toInt($admin['turntime'])); } - - $now = TimeUtil::now(true); if ($now >= $turntime) { $turntime = addTurn($turntime, $admin['turnterm']); } diff --git a/hwe/sammo/API/Global/ExecuteEngine.php b/hwe/sammo/API/Global/ExecuteEngine.php index 192d1cd3..de554acf 100644 --- a/hwe/sammo/API/Global/ExecuteEngine.php +++ b/hwe/sammo/API/Global/ExecuteEngine.php @@ -8,6 +8,8 @@ use DateTimeInterface; use sammo\Enums\APIRecoveryType; use sammo\TurnExecutionHelper; use sammo\UniqueConst; +use sammo\GameClock; +use sammo\KVStorage; class ExecuteEngine extends \sammo\BaseAPI { @@ -35,11 +37,13 @@ class ExecuteEngine extends \sammo\BaseAPI $updated = false; $locked = false; $lastExecuted = TurnExecutionHelper::executeAllCommand($updated, $locked); + $clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); return [ 'result' => true, 'updated' => $updated, 'locked' => $locked, - 'lastExecuted' => $lastExecuted, + 'lastExecutedTick' => $lastExecuted, + 'lastExecuted' => $clock->formatTick($lastExecuted, true), ]; } } diff --git a/hwe/sammo/API/Global/GeneralList.php b/hwe/sammo/API/Global/GeneralList.php index 1b5ca061..85a77db8 100644 --- a/hwe/sammo/API/Global/GeneralList.php +++ b/hwe/sammo/API/Global/GeneralList.php @@ -3,6 +3,7 @@ namespace sammo\API\Global; use sammo\DB; +use sammo\GameClock; use sammo\Enums\APIRecoveryType; use sammo\Json; use sammo\KVStorage; @@ -145,7 +146,8 @@ class GeneralList extends \sammo\BaseAPI if (static::$withToken) { - $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s'); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $now = GameClock::fromStorage($gameStor)->nowTick(); $tokens = []; foreach ($db->query('SELECT * FROM select_npc_token WHERE `valid_until`>=%s', $now) as $token) { $validUntil = $token['valid_until']; diff --git a/hwe/sammo/API/InheritAction/ResetTurnTime.php b/hwe/sammo/API/InheritAction/ResetTurnTime.php index 3f8e7a20..1419ba13 100644 --- a/hwe/sammo/API/InheritAction/ResetTurnTime.php +++ b/hwe/sammo/API/InheritAction/ResetTurnTime.php @@ -64,13 +64,11 @@ class ResetTurnTime extends \sammo\BaseAPI $gameStor = KVStorage::getStorage($db, 'game_env'); $turnTerm = $gameStor->getValue('turnterm'); - $currTurnTime = new DateTimeImmutable($general->getTurnTime()); - $rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize( UniqueConst::$hiddenSeed, 'ResetTurnTime', $userID, - $general->getAuxVar('nextTurnTimeBase') ?? $general->getTurnTime() + $general->getAuxVar('nextTurnTimeBase') ?? $general->getTurnTick() ))); $afterTurn = $rng->nextFloat1() * $turnTerm * 60; diff --git a/hwe/sammo/API/Nation/GeneralList.php b/hwe/sammo/API/Nation/GeneralList.php index 928340ce..26001519 100644 --- a/hwe/sammo/API/Nation/GeneralList.php +++ b/hwe/sammo/API/Nation/GeneralList.php @@ -8,6 +8,7 @@ use sammo\Enums\APIRecoveryType; use sammo\Enums\GeneralLiteQueryMode; use sammo\Enums\GeneralQueryMode; use sammo\General; +use sammo\GameClock; use sammo\GeneralLite; use sammo\Session; use sammo\Util; @@ -264,8 +265,11 @@ class GeneralList extends \sammo\BaseAPI 'honorText' => fn ($rawGeneral) => getHonor($rawGeneral['experience']), 'dedLevelText' => fn ($rawGeneral) => getDedLevelText($rawGeneral['dedlevel']), //'0000-00-00 11:23'; - 'turntime' => fn ($rawGeneral) => substr($rawGeneral['turntime'], 0, 19), - 'recent_war' => fn ($rawGeneral) => substr($rawGeneral['recent_war'], 0, 19), + 'turntime' => fn ($rawGeneral) => GameClock::fromStorage($gameStor) + ->formatTick(Util::toInt($rawGeneral['turntime'])), + 'recent_war' => fn ($rawGeneral) => $rawGeneral['recent_war'] === null + ? null + : GameClock::fromStorage($gameStor)->formatTick(Util::toInt($rawGeneral['recent_war'])), 'bill' => fn ($rawGeneral) => getBillByLevel($rawGeneral['dedlevel']), 'reservedCommand' => fn ($rawGeneral) => $reservedCommand[$rawGeneral['no']] ?? null, 'autorun_limit' => fn ($rawGeneral) => ($rawGeneral['aux'] ?? [])['autorun_limit'] ?? 0, diff --git a/hwe/sammo/API/NationCommand/GetReservedCommand.php b/hwe/sammo/API/NationCommand/GetReservedCommand.php index 8930329b..78af94eb 100644 --- a/hwe/sammo/API/NationCommand/GetReservedCommand.php +++ b/hwe/sammo/API/NationCommand/GetReservedCommand.php @@ -8,6 +8,7 @@ use sammo\DB; use sammo\Enums\APIRecoveryType; use sammo\Enums\GeneralQueryMode; use sammo\GameConst; +use sammo\GameClock; use sammo\General; use sammo\Json; use sammo\KVStorage; @@ -51,7 +52,8 @@ class GetReservedCommand extends \sammo\BaseAPI $nationID = $me['nation']; $limitState = checkLimit($me['refresh_score']); if ($limitState >= 2) { - return "접속 제한중입니다. 1턴 이내에 너무 많은 갱신을 하셨습니다. (다음 갱신 가능 시각 : {$me['turntime']})"; + $limitTime = GameClock::fromStorage($gameStor)->formatTick(Util::toInt($me['turntime']), true); + return "접속 제한중입니다. 1턴 이내에 너무 많은 갱신을 하셨습니다. (다음 갱신 가능 시각 : {$limitTime})"; } $permission = checkSecretPermission($me); diff --git a/hwe/sammo/AbsFromUserPool.php b/hwe/sammo/AbsFromUserPool.php index 274b79b0..eec33d99 100644 --- a/hwe/sammo/AbsFromUserPool.php +++ b/hwe/sammo/AbsFromUserPool.php @@ -30,8 +30,9 @@ abstract class AbsFromUserPool extends AbsGeneralPool{ } static public function pickGeneralFromPool(\MeekroDB $db, RandUtil $rng, int $owner, int $pickCnt, ?string $prefix=null):array{ - $oNow = new \DateTimeImmutable(); - $now = $oNow->format('Y-m-d H:i:s'); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); + $now = $clock->nowTick(); $db->update('select_pool', [ 'reserved_until'=>null, @@ -48,9 +49,8 @@ abstract class AbsFromUserPool extends AbsGeneralPool{ throw new \RuntimeException('pool 부족'); } - $gameStor = KVStorage::getStorage($db, 'game_env'); - $result = []; - $validUntil = TimeUtil::nowAddMinutes(2 * $gameStor->turnterm); + $result = []; + $validUntil = $now + GameClock::TICKS_PER_TURN * 2; while(count($result) < $pickCnt){ $cand = $rng->choiceUsingWeightPair($pool); $poolID = $cand['id']; diff --git a/hwe/sammo/AbsGeneralPool.php b/hwe/sammo/AbsGeneralPool.php index 64252ed3..049b13b2 100644 --- a/hwe/sammo/AbsGeneralPool.php +++ b/hwe/sammo/AbsGeneralPool.php @@ -34,7 +34,7 @@ abstract class AbsGeneralPool{ * specialWar */ - public function __construct(\MeekroDB $db, RandUtil $rng, array $info, string $validUntil) + public function __construct(\MeekroDB $db, RandUtil $rng, array $info, int $validUntil) { $this->db = $db; $this->info = $info; @@ -92,7 +92,7 @@ abstract class AbsGeneralPool{ return $this->builder; } - public function getValidUntil():string{ + public function getValidUntil():int{ return $this->validUntil; } @@ -109,4 +109,4 @@ abstract class AbsGeneralPool{ abstract public static function getPoolName():string; abstract public static function initPool(\MeekroDB $db); -} \ No newline at end of file +} diff --git a/hwe/sammo/Auction.php b/hwe/sammo/Auction.php index ce274efa..8f412b7e 100644 --- a/hwe/sammo/Auction.php +++ b/hwe/sammo/Auction.php @@ -146,40 +146,35 @@ abstract class Auction return $this->info; } - public function shrinkCloseDate(?DateTimeInterface $date): ?string + public function shrinkCloseTick(?int $tick): ?string { - if ($date === null) { - $date = new DateTimeImmutable(); - } - - $this->info->closeDate = $date; $db = DB::db(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); + $this->info->closeTick = $tick ?? $clock->nowTick(); $db->update('ng_auction', $this->info->toArray('id'), 'id = %i', $this->info->id); return null; } - public function extendLatestBidCloseDate(?DateTimeInterface $date): ?string + public function extendLatestBidCloseTick(?int $tick): ?string { - if ($date === null) { + if ($tick === null) { $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); $turnTerm = $gameStor->getValue('turnterm'); - $date = $this->info->closeDate->add(TimeUtil::secondsToDateInterval( - max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60 - )); + $tick = $this->info->closeTick + $clock->ticksFromMinutes( + max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) + ); } - else{ - $date = DateTimeImmutable::createFromInterface($date); - } - if ($this->info->detail->availableLatestBidCloseDate !== null && $date < $this->info->detail->availableLatestBidCloseDate) { + if ($this->info->detail->availableLatestBidCloseTick !== null && $tick < $this->info->detail->availableLatestBidCloseTick) { return '기간보다 짧습니다.'; } - $this->info->detail->availableLatestBidCloseDate = $date; + $this->info->detail->availableLatestBidCloseTick = $tick; return null; } - public function extendCloseDate(DateTimeInterface $date, bool $force = false): ?string + public function extendCloseTick(int $tick, bool $force = false): ?string { if (!$force) { if ($this->info->detail->remainCloseDateExtensionCnt === null) { @@ -193,12 +188,11 @@ abstract class Auction } } - if ($date < $this->info->closeDate) { + if ($tick < $this->info->closeTick) { return '종료 기간보다 짧습니다.'; } - $closeDate = DateTimeImmutable::createFromInterface($date); - $this->info->closeDate = $closeDate; + $this->info->closeTick = $tick; return null; } @@ -245,12 +239,13 @@ abstract class Auction //TODO: 전역 알림이 나타나야한다. 일반 메시지보다는 중요하고, 메시지보단 약하게.. //TODO: 바로가기를 제공하는 편이 좋을 것 같다. + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); $msg = new Message( MessageType::private, $src, $dest, $reason, - new DateTime(), + DateTime::createFromImmutable($clock->tickToDateTime($clock->nowTick())), new DateTime('9999-12-31'), [] ); @@ -275,7 +270,12 @@ abstract class Auction $db->update('ng_auction', $this->info->toArray('id'), 'id = %i', $this->info->id); } - private function bidInheritPoint(int $amount, \DateTimeImmutable $now, bool $tryExtendCloseDate): ?string + private function bidInheritPoint( + int $amount, + int $nowTick, + \DateTimeImmutable $nowDate, + bool $tryExtendCloseDate, + ): ?string { $db = DB::db(); @@ -311,7 +311,7 @@ abstract class Auction $general->getVar('owner'), $general->getID(), $amount, - $now, + $nowDate, new AuctionBidItemData( $general->getVar('owner_name'), $obfuscatedName, @@ -324,15 +324,16 @@ abstract class Auction } $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); $turnTerm = $gameStor->getValue('turnterm'); - if ($this->info->detail->availableLatestBidCloseDate !== null) { - $extendedCloseDate = $now->add(TimeUtil::secondsToDateInterval( - max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60 - )); + if ($this->info->detail->availableLatestBidCloseTick !== null) { + $extendedCloseTick = $nowTick + $clock->ticksFromMinutes( + max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) + ); - if ($extendedCloseDate > $this->info->closeDate && $this->info->closeDate < $this->info->detail->availableLatestBidCloseDate) { - $this->extendCloseDate(min($extendedCloseDate, $this->info->detail->availableLatestBidCloseDate), true); + if ($extendedCloseTick > $this->info->closeTick && $this->info->closeTick < $this->info->detail->availableLatestBidCloseTick) { + $this->extendCloseTick(min($extendedCloseTick, $this->info->detail->availableLatestBidCloseTick), true); $this->applyDB(); } } @@ -356,12 +357,16 @@ abstract class Auction return '경매가 이미 끝났습니다.'; } - $now = new \DateTimeImmutable(); + $db = DB::db(); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); + $nowTick = $clock->nowTick(); + $nowDate = $clock->tickToDateTime($nowTick); - if ($auctionInfo->closeDate < $now) { + if ($auctionInfo->closeTick < $nowTick) { return '경매가 이미 끝났습니다.'; } - if ($auctionInfo->openDate > $now) { + if ($auctionInfo->openTick > $nowTick) { return '경매가 아직 시작되지 않았습니다.'; } @@ -377,13 +382,11 @@ abstract class Auction if ($auctionInfo->reqResource === ResourceType::inheritancePoint) { - return $this->bidInheritPoint($amount, $now, $tryExtendCloseDate); + return $this->bidInheritPoint($amount, $nowTick, $nowDate, $tryExtendCloseDate); } //reqResource는 말 그대로 '구매자가 내야하는 자원'이다. - $db = DB::db(); - $highestBid = $this->getHighestBid(); if (!$auctionInfo->detail->isReverse) { if ($highestBid !== null && $amount <= $highestBid->amount) { @@ -421,7 +424,7 @@ abstract class Auction $general->getVar('owner'), $general->getID(), $amount, - $now, + $nowDate, new AuctionBidItemData( $general->getVar('owner_name'), $general->getName(), @@ -436,14 +439,13 @@ abstract class Auction $general->increaseVar($resType->value, -$morePoint); - $gameStor = KVStorage::getStorage($db, 'game_env'); $turnTerm = $gameStor->getValue('turnterm'); - $extendedCloseDate = $now->add(TimeUtil::secondsToDateInterval( - max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60 - )); + $extendedCloseTick = $nowTick + $clock->ticksFromMinutes( + max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) + ); - if ($extendedCloseDate > $this->info->closeDate) { - $this->extendCloseDate($extendedCloseDate, true); + if ($extendedCloseTick > $this->info->closeTick) { + $this->extendCloseTick($extendedCloseTick, true); $this->applyDB(); } @@ -456,10 +458,10 @@ abstract class Auction public function tryFinish(): ?bool { - $now = new DateTimeImmutable(); - if ($now < $this->info->closeDate) { - return null; - } + $db = DB::db(); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); + if ($clock->nowTick() < $this->info->closeTick) return null; //경매를 닫아야한다. $highestBid = $this->getHighestBid(); @@ -469,17 +471,15 @@ abstract class Auction } if ($highestBid->aux->tryExtendCloseDate) { - $db = DB::db(); - $gameStor = KVStorage::getStorage($db, 'game_env'); $turnTerm = $gameStor->getValue('turnterm'); //연장 요청이 있었다. - $extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval( - max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_BY_EXTENSION_QUERY) * 60 - )); + $extendedCloseTick = $this->info->closeTick + $clock->ticksFromMinutes( + max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_BY_EXTENSION_QUERY) + ); - if ($this->extendCloseDate($extendedCloseDate) === null) { - $this->extendLatestBidCloseDate(null); + if ($this->extendCloseTick($extendedCloseTick) === null) { + $this->extendLatestBidCloseTick(null); $this->applyDB(); return false; } @@ -509,12 +509,13 @@ abstract class Auction //TODO: 전역 알림이 나타나야한다. 일반 메시지보다는 중요하고, 메시지보단 약하게.. //TODO: 바로가기를 제공하는 편이 좋을 것 같다. + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); $msg = new Message( MessageType::private, $src, $dest, $failReason, - new \DateTime(), + DateTime::createFromImmutable($clock->tickToDateTime($clock->nowTick())), new \DateTime('9999-12-31'), [] ); diff --git a/hwe/sammo/AuctionBasicResource.php b/hwe/sammo/AuctionBasicResource.php index 4302a1b8..fc6cb427 100644 --- a/hwe/sammo/AuctionBasicResource.php +++ b/hwe/sammo/AuctionBasicResource.php @@ -56,10 +56,11 @@ abstract class AuctionBasicResource extends Auction } - $now = new \DateTimeImmutable(); $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); + $nowTick = $clock->nowTick(); $turnTerm = $gameStor->getValue('turnterm'); - $closeDate = $now->add(TimeUtil::secondsToDateInterval($closeTurnCnt * $turnTerm * 60)); + $closeTick = $nowTick + GameClock::TICKS_PER_TURN * $closeTurnCnt; $openResult = static::openAuction(new AuctionInfo( null, @@ -68,8 +69,8 @@ abstract class AuctionBasicResource extends Auction "$amount", $general->getId(), $bidderRes, - $now, - $closeDate, + $nowTick, + $closeTick, new AuctionInfoDetail( "{$hostResName} {$amount} 경매", $general->getName(), @@ -145,12 +146,13 @@ abstract class AuctionBasicResource extends Auction //TODO: 전역 알림이 나타나야한다. 일반 메시지보다는 중요하고, 메시지보단 약하게.. //TODO: 바로가기를 제공하는 편이 좋을 것 같다. + $clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); $msg = new Message( MessageType::private, $src, $dest, "{$this->auctionID}번 {$hostResName} 경매에 입찰이 없어 취소되었습니다.", - new \DateTime(), + \DateTime::createFromImmutable($clock->tickToDateTime($clock->nowTick())), new \DateTime('9999-12-31'), [] ); @@ -246,8 +248,8 @@ abstract class AuctionBasicResource extends Auction $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); $turnTerm = $gameStor->getValue('turnterm'); - $date = (new DateTimeImmutable())->add(TimeUtil::secondsToDateInterval($turnTerm * 60)); - $this->shrinkCloseDate($date); + $clock = GameClock::fromStorage($gameStor); + $this->shrinkCloseTick($clock->nowTick() + GameClock::TICKS_PER_TURN); } return null; diff --git a/hwe/sammo/AuctionUniqueItem.php b/hwe/sammo/AuctionUniqueItem.php index 069bd346..74231ad4 100644 --- a/hwe/sammo/AuctionUniqueItem.php +++ b/hwe/sammo/AuctionUniqueItem.php @@ -72,16 +72,15 @@ class AuctionUniqueItem extends Auction $gameStor = KVStorage::getStorage($db, 'game_env'); - $now = new DateTimeImmutable(); - [$turnTerm, $year, $month] = $gameStor->getValuesAsArray(['turnterm', 'year', 'month']); - - $closeDate = $now->add(TimeUtil::secondsToDateInterval( - max(static::MIN_AUCTION_CLOSE_MINUTES, $turnTerm * static::COEFF_AUCTION_CLOSE_MINUTES) * 60 - )); - $availableLatestBidCloseDate = $closeDate->add(TimeUtil::secondsToDateInterval( - max(static::MIN_EXTENSION_MINUTES_LIMIT_BY_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_BY_BID) * 60 - )); + $clock = GameClock::fromStorage($gameStor); + $nowTick = $clock->nowTick(); + $closeTick = $nowTick + $clock->ticksFromMinutes( + max(static::MIN_AUCTION_CLOSE_MINUTES, $turnTerm * static::COEFF_AUCTION_CLOSE_MINUTES) + ); + $availableLatestBidCloseTick = $closeTick + $clock->ticksFromMinutes( + max(static::MIN_EXTENSION_MINUTES_LIMIT_BY_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_BY_BID) + ); $info = new AuctionInfo( null, @@ -90,8 +89,8 @@ class AuctionUniqueItem extends Auction $itemKey, $general->getID(), ResourceType::inheritancePoint, - $now, - $closeDate, + $nowTick, + $closeTick, new AuctionInfoDetail( "{$item->getName()} 경매", static::genObfuscatedName($general->getID()), @@ -100,7 +99,7 @@ class AuctionUniqueItem extends Auction $startAmount, null, 1, - $availableLatestBidCloseDate, + $availableLatestBidCloseTick, ) ); @@ -267,21 +266,22 @@ class AuctionUniqueItem extends Auction if ($availableEquipUniqueCnt <= 0) { $turnTerm = $gameStor->getValue('turnterm'); + $clock = GameClock::fromStorage($gameStor); //제한에 걸렸다면 자동 연장 - $extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval( - max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_UNIQUE_CNT) * 60 - )); + $extendedCloseTick = $this->info->closeTick + $clock->ticksFromMinutes( + max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_UNIQUE_CNT) + ); if($bidder->getID() != $this->info->hostGeneralID){ $this->setHostAsNeutral(); } - $this->extendCloseDate($extendedCloseDate, true); - $this->extendLatestBidCloseDate(null); + $this->extendCloseTick($extendedCloseTick, true); + $this->extendLatestBidCloseTick(null); $this->applyDB(); return '유니크 아이템 소유 제한 상태입니다. 종료 시간이 연장됩니다.'; } - $isExtendCloseDateRequired = false; + $isExtendCloseTickRequired = false; foreach (GameConst::$allItems as $itemType => $itemList) { //아직은 그런 경우는 없지만 동일 유니크를 여러 부위에 장착할 수 있을지도 모름 if (!key_exists($itemKey, $itemList)) { @@ -291,13 +291,13 @@ class AuctionUniqueItem extends Auction $ownItem = $general->getItem($itemType); if ($ownItem->getRawClassName() == $itemKey) { //FIXME: 이 경우에는 환불이 되던가 해야함. - $isExtendCloseDateRequired = true; + $isExtendCloseTickRequired = true; $reasons[] = '이미 그 유니크를 가지고 있습니다.'; continue; } if (!$ownItem->isBuyable()) { - $isExtendCloseDateRequired = true; + $isExtendCloseTickRequired = true; $reasons[] = '이미 다른 유니크를 가지고 있습니다.'; continue; } @@ -313,18 +313,19 @@ class AuctionUniqueItem extends Auction } if (!$availableItemTypes) { - if ($isExtendCloseDateRequired) { + if ($isExtendCloseTickRequired) { $turnTerm = $gameStor->getValue('turnterm'); + $clock = GameClock::fromStorage($gameStor); //동일 부위 제한에 걸렸다면 자동 연장 - $extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval( - max(static::MIN_EXTENSION_MINUTES_LIMIT_BY_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_BY_BID) * 60 - )); + $extendedCloseTick = $this->info->closeTick + $clock->ticksFromMinutes( + max(static::MIN_EXTENSION_MINUTES_LIMIT_BY_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_BY_BID) + ); if($bidder->getID() != $this->info->hostGeneralID){ $this->setHostAsNeutral(); } - $this->extendCloseDate($extendedCloseDate, true); - $this->extendLatestBidCloseDate(null); + $this->extendCloseTick($extendedCloseTick, true); + $this->extendLatestBidCloseTick(null); $this->applyDB(); } return join(' ', $reasons); diff --git a/hwe/sammo/Command/Nation/che_발령.php b/hwe/sammo/Command/Nation/che_발령.php index f8598585..3d6af631 100644 --- a/hwe/sammo/Command/Nation/che_발령.php +++ b/hwe/sammo/Command/Nation/che_발령.php @@ -159,7 +159,7 @@ class che_발령 extends Command\NationCommand $destGeneral->getLogger()->pushGeneralActionLog("{$generalName}에 의해 {$destCityName}{$josaRo} 발령됐습니다. <1>$date"); $yearMonth = Util::joinYearMonth($this->env['year'], $this->env['month']); - if (cutTurn($general->getTurnTime(), $this->env['turnterm']) != cutTurn($destGeneral->getTurnTime(), $this->env['turnterm'])) { + if (cutTurn($general->getTurnTick(), $this->env['turnterm']) != cutTurn($destGeneral->getTurnTick(), $this->env['turnterm'])) { $yearMonth += 1; } $destGeneral->setAuxVar('last발령', $yearMonth); diff --git a/hwe/sammo/Command/Nation/che_천도.php b/hwe/sammo/Command/Nation/che_천도.php index ac871db3..1e0ef891 100644 --- a/hwe/sammo/Command/Nation/che_천도.php +++ b/hwe/sammo/Command/Nation/che_천도.php @@ -137,7 +137,7 @@ class che_천도 extends Command\NationCommand $nationID = $general->getNationID(); $nationStor = \sammo\KVStorage::getStorage(DB::db(), $nationID, 'nation_env'); - $nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTime()]; + $nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTick()]; if ($lastTurn->getCommand() != $commandName || $lastTurn->getArg() !== $this->arg) { $this->setResultTurn(new LastTurn( diff --git a/hwe/sammo/DTO/AuctionInfo.php b/hwe/sammo/DTO/AuctionInfo.php index dc822137..61aff7c3 100644 --- a/hwe/sammo/DTO/AuctionInfo.php +++ b/hwe/sammo/DTO/AuctionInfo.php @@ -2,11 +2,9 @@ namespace sammo\DTO; -use LDTO\Attr\Convert; use LDTO\Attr\JsonString; use LDTO\Attr\NullIsUndefined; use LDTO\Attr\RawName; -use LDTO\Converter\DateTimeConverter; use sammo\Enums\AuctionType; use sammo\Enums\ResourceType; @@ -23,12 +21,10 @@ class AuctionInfo extends \LDTO\DTO #[RawName('req_resource')] public ResourceType $reqResource, - #[RawName('open_date')] - #[Convert(DateTimeConverter::class)] - public \DateTimeImmutable $openDate, - #[RawName('close_date')] - #[Convert(DateTimeConverter::class)] - public \DateTimeImmutable $closeDate, + #[RawName('open_tick')] + public int $openTick, + #[RawName('close_tick')] + public int $closeTick, #[JsonString] public AuctionInfoDetail $detail, diff --git a/hwe/sammo/DTO/AuctionInfoDetail.php b/hwe/sammo/DTO/AuctionInfoDetail.php index 4f1c9f68..8b27cbd1 100644 --- a/hwe/sammo/DTO/AuctionInfoDetail.php +++ b/hwe/sammo/DTO/AuctionInfoDetail.php @@ -2,9 +2,7 @@ namespace sammo\DTO; -use LDTO\Attr\Convert; use LDTO\Attr\NullIsUndefined; -use LDTO\Converter\DateTimeConverter; class AuctionInfoDetail extends \LDTO\DTO { @@ -21,8 +19,7 @@ class AuctionInfoDetail extends \LDTO\DTO #[NullIsUndefined] public ?int $remainCloseDateExtensionCnt, #[NullIsUndefined] - #[Convert(DateTimeConverter::class)] - public ?\DateTimeImmutable $availableLatestBidCloseDate, + public ?int $availableLatestBidCloseTick, ) { } } diff --git a/hwe/sammo/DTO/GeneralAccessLog.php b/hwe/sammo/DTO/GeneralAccessLog.php index d70731f6..f1210726 100644 --- a/hwe/sammo/DTO/GeneralAccessLog.php +++ b/hwe/sammo/DTO/GeneralAccessLog.php @@ -2,10 +2,8 @@ namespace sammo\DTO; -use LDTO\Attr\Convert; use LDTO\Attr\NullIsUndefined; use LDTO\Attr\RawName; -use LDTO\Converter\DateTimeConverter; class GeneralAccessLog extends \LDTO\DTO { @@ -20,8 +18,7 @@ class GeneralAccessLog extends \LDTO\DTO public ?int $userID, #[RawName('last_refresh')] - #[Convert(DateTimeConverter::class)] - public \DateTimeImmutable $lastRefresh, + public ?int $lastRefresh, public int $refresh, @@ -35,4 +32,4 @@ class GeneralAccessLog extends \LDTO\DTO public int $refreshScoreTotal, ) { } -} \ No newline at end of file +} diff --git a/hwe/sammo/DiplomaticMessage.php b/hwe/sammo/DiplomaticMessage.php index 7d9a2596..a439efa1 100644 --- a/hwe/sammo/DiplomaticMessage.php +++ b/hwe/sammo/DiplomaticMessage.php @@ -49,7 +49,7 @@ class DiplomaticMessage extends Message{ $this->validDiplomacy = false; } - if($this->validUntil < (new \DateTime())){ + if($this->validUntil < $this->date){ $this->validDiplomacy = false; } } @@ -281,4 +281,4 @@ class DiplomaticMessage extends Message{ return self::DECLINED; } -} \ No newline at end of file +} diff --git a/hwe/sammo/General.php b/hwe/sammo/General.php index 43a35816..3d7ec035 100644 --- a/hwe/sammo/General.php +++ b/hwe/sammo/General.php @@ -281,16 +281,13 @@ class General extends GeneralBase implements iAction $this->calcCache[$cacheKey] = $result; return $result; } - $recwar = new \DateTimeImmutable($this->getVar('recent_war')); - $turnNow = new \DateTimeImmutable($this->getVar('turntime')); - $secDiff = TimeUtil::DateIntervalToSeconds($recwar->diff($turnNow)); - - if ($secDiff <= 0) { - $this->calcCache[$cacheKey] = 0; - return 0; - } - - $result = intdiv(Util::toInt($secDiff), 60 * $turnTerm); + $tickDiff = Util::toInt($this->getVar('turntime')) - Util::toInt($this->getVar('recent_war')); + if ($tickDiff <= 0) { + $this->calcCache[$cacheKey] = 0; + return 0; + } + + $result = intdiv($tickDiff, GameClock::TICKS_PER_TURN); $this->calcCache[$cacheKey] = $result; return $result; } diff --git a/hwe/sammo/GeneralAI.php b/hwe/sammo/GeneralAI.php index 5364c7b1..9a54f03d 100644 --- a/hwe/sammo/GeneralAI.php +++ b/hwe/sammo/GeneralAI.php @@ -302,7 +302,7 @@ class GeneralAI $this->calcWarRoute(); $troopCandidate = []; - $chiefTurn = cutTurn($this->general->getTurnTime(), $this->env['turnterm']); + $chiefTurn = cutTurn($this->general->getTurnTick(), $this->env['turnterm']); $yearMonth = Util::joinYearMonth($this->env['year'], $this->env['month']); foreach ($this->troopLeaders as $troopLeader) { @@ -319,7 +319,7 @@ class GeneralAI $last발령 = $troopLeader->getAuxVar('last발령'); if ($last발령) { - $leaderTurn = cutTurn($troopLeader->getTurnTime(), $this->env['turnterm']); + $leaderTurn = cutTurn($troopLeader->getTurnTick(), $this->env['turnterm']); $compYearMonth = $yearMonth; if ($chiefTurn < $leaderTurn) { $compYearMonth += 1; @@ -405,7 +405,7 @@ class GeneralAI return null; } - $chiefTurn = cutTurn($this->general->getTurnTime(), $this->env['turnterm']); + $chiefTurn = cutTurn($this->general->getTurnTick(), $this->env['turnterm']); $yearMonth = Util::joinYearMonth($this->env['year'], $this->env['month']); $troopCandidate = []; @@ -428,7 +428,7 @@ class GeneralAI $last발령 = $troopLeader->getAuxVar('last발령'); if ($last발령) { - $leaderTurn = cutTurn($troopLeader->getTurnTime(), $this->env['turnterm']); + $leaderTurn = cutTurn($troopLeader->getTurnTick(), $this->env['turnterm']); $compYearMonth = $yearMonth; if ($chiefTurn < $leaderTurn) { $compYearMonth += 1; @@ -586,8 +586,8 @@ class GeneralAI continue; } - $generalTurnTime = $userGeneral->getTurnTime(); - $troopTurnTime = $troopLeader->getTurnTime(); + $generalTurnTime = $userGeneral->getTurnTick(); + $troopTurnTime = $troopLeader->getTurnTick(); if ($generalTurnTime < $troopTurnTime) { //NOTE: 어차피 수뇌 턴이 제일 빠르다 $generalCadidates[$generalID] = $userGeneral; @@ -782,7 +782,7 @@ class GeneralAI if ( key_exists($troopLeader->getCityID(), $this->supplyCities) && - $this->troopLeaders[$troopID]->getTurnTime() < $lostGeneral->getTurnTime() + $this->troopLeaders[$troopID]->getTurnTick() < $lostGeneral->getTurnTick() ) { //이미 탈출 가능한 부대를 탔다 continue; @@ -1986,7 +1986,7 @@ class GeneralAI if ($lastTurn->getCommand() === '천도' && $lastTurn->getArg()['destCityID'] != $this->nation['capital']) { $cmd = buildNationCommandClass('che_천도', $this->general, $this->env, $lastTurn, $lastTurn->getArg()); if ($cmd->hasFullConditionMet()) { - $nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTime()]; + $nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTick()]; $this->reqUpdateInstance = true; return $cmd; } @@ -1995,12 +1995,9 @@ class GeneralAI $lastTrial = $nationStor->last천도Trial; if ($lastTrial) { [$lastTrialLevel, $lastTrialTurnTime] = $lastTrial; - $timeDiffSeconds = TimeUtil::DateIntervalToSeconds( - date_create_immutable($lastTrialTurnTime)->diff( - date_create_immutable($general->getTurnTime()) - ) - ); - if ($timeDiffSeconds < $turnTerm * 30 && $lastTrialLevel !== $general->getVar('officer_level')) { //0.5Turn + $timeDiffTick = abs($general->getTurnTick() - Util::toInt($lastTrialTurnTime)); + if ($timeDiffTick < intdiv(GameClock::TICKS_PER_TURN, 2) + && $lastTrialLevel !== $general->getVar('officer_level')) { //0.5Turn return null; } } @@ -2109,7 +2106,7 @@ class GeneralAI } - $nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTime()]; + $nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTick()]; $this->reqUpdateInstance = true; return $cmd; } diff --git a/hwe/sammo/GeneralBase.php b/hwe/sammo/GeneralBase.php index 9b9c6757..8a3bf621 100644 --- a/hwe/sammo/GeneralBase.php +++ b/hwe/sammo/GeneralBase.php @@ -54,14 +54,24 @@ abstract class GeneralBase ); } - function getTurnTime(int $short = self::TURNTIME_FULL_MS): ?string - { - if(!key_exists('turntime', $this->raw)){ - return null; - } - - return [ - self::TURNTIME_FULL_MS => function ($turntime) { + function getTurnTime(int $short = self::TURNTIME_FULL_MS): ?string + { + if(!key_exists('turntime', $this->raw)){ + return null; + } + + $rawTurnTime = $this->getVar('turntime'); + // 비교용 Dummy와 과거 fixture는 문자열을 잠시 허용하되 제품 DB는 tick만 사용합니다. + if (is_string($rawTurnTime) && !ctype_digit(ltrim($rawTurnTime, '-'))) { + $formattedTurnTime = $rawTurnTime; + } else { + $db = DB::db(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); + $formattedTurnTime = $clock->formatTick(Util::toInt($rawTurnTime), true); + } + + return [ + self::TURNTIME_FULL_MS => function ($turntime) { return $turntime; }, self::TURNTIME_FULL => function ($turntime) { @@ -73,8 +83,21 @@ abstract class GeneralBase self::TURNTIME_HM => function ($turntime) { return substr($turntime, 11, 5); }, - ][$short]($this->getVar('turntime')); - } + ][$short]($formattedTurnTime); + } + + function getTurnTick(): ?int + { + if (!key_exists('turntime', $this->raw)) { + return null; + } + $rawTurnTime = $this->getVar('turntime'); + if (is_string($rawTurnTime) && !ctype_digit(ltrim($rawTurnTime, '-'))) { + $clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); + return $clock->dateTimeToTick(new \DateTimeImmutable($rawTurnTime)); + } + return Util::toInt($rawTurnTime); + } function getNPCType(): int { diff --git a/hwe/sammo/GeneralPool/RandomNameGeneral.php b/hwe/sammo/GeneralPool/RandomNameGeneral.php index ffdd7012..0afc852e 100644 --- a/hwe/sammo/GeneralPool/RandomNameGeneral.php +++ b/hwe/sammo/GeneralPool/RandomNameGeneral.php @@ -59,7 +59,7 @@ class RandomNameGeneral extends AbsGeneralPool{ 'generalName'=>$generalName, 'imgsvr'=>0, 'picture'=>null - ], '9999-12-31 12:00:00'); + ], PHP_INT_MAX); } static public function pickGeneralFromPool(MeekroDB $db, RandUtil $rng, int $owner, int $pickCnt, ?string $prefix = null): array @@ -68,26 +68,25 @@ class RandomNameGeneral extends AbsGeneralPool{ $result = []; $dbInsert = []; - $oNow = new \DateTimeImmutable(); - - for($i=0;$i<$pickCnt;$i++){ $result[] = static::pickGeneral1FromPool($db, $rng, $owner, $prefix); } if($owner){ - $now = $oNow->format('Y-m-d H:i:s'); + $gameStor = \sammo\KVStorage::getStorage($db, 'game_env'); + $clock = \sammo\GameClock::fromStorage($gameStor); + $now = $clock->nowTick(); $db->delete('select_pool', [ 'reserved_until'=>null, 'owner'=>null, ],'(reserved_until < %s OR reserved_until IS NULL) AND general_id IS null', $now); - $validUntil = $oNow->add(new \DateInterval(sprintf('PT%dS', 30))); + $validUntil = $now + $clock->ticksFromSeconds(30); foreach($result as $pickedGeneral){ $dbInsert[] = [ 'owner'=>$owner, 'uniqueName'=>$pickedGeneral->getUniqueName(), 'info'=>$pickedGeneral->getInfo(), - 'reserved_until'=>$validUntil->format(('Y-m-d H:i:s')) + 'reserved_until'=>$validUntil ]; } $db->insert('select_pool', $dbInsert); @@ -99,4 +98,4 @@ class RandomNameGeneral extends AbsGeneralPool{ public static function initPool(\MeekroDB $db){ //do Nothing } -} \ No newline at end of file +} diff --git a/hwe/sammo/GeneralPool/SPoolUnderU100.php b/hwe/sammo/GeneralPool/SPoolUnderU100.php index 6a4becfe..3953acf6 100644 --- a/hwe/sammo/GeneralPool/SPoolUnderU100.php +++ b/hwe/sammo/GeneralPool/SPoolUnderU100.php @@ -14,7 +14,7 @@ class SPoolUnderU100 extends AbsFromUserPool private const STAT_BONUS_MAX_TOTAL = 190; private const STAT_BONUS_MAX_MULTIPLIER = 1.5; - public function __construct(\MeekroDB $db, RandUtil $rng, array $info, string $validUntil) + public function __construct(\MeekroDB $db, RandUtil $rng, array $info, int $validUntil) { $targetInfo = $info; $initialInfo = $info; diff --git a/hwe/sammo/Message.php b/hwe/sammo/Message.php index c83ad34b..169e4103 100644 --- a/hwe/sammo/Message.php +++ b/hwe/sammo/Message.php @@ -104,6 +104,7 @@ class Message public static function buildFromArray(array $row) : Message { + $clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); $dbMessage = Json::decode($row['message']); $msgType = MessageType::from($row['type']); @@ -116,8 +117,8 @@ class Message $src, $dest, $dbMessage['text'], - new \DateTime($row['time']), - new \DateTime($row['valid_until']), + \DateTime::createFromImmutable($clock->tickToDateTime(Util::toInt($row['time']))), + \DateTime::createFromImmutable($clock->tickToDateTime(Util::toInt($row['valid_until']))), $option ]; @@ -151,9 +152,12 @@ class Message public static function getMessageByID(int $messageID) : ?Message { $db = DB::db(); - $now = new \DateTime(); - $row = $db->queryFirstRow('SELECT * FROM `message` WHERE `id` = %i AND valid_until', $messageID); - //FIXME: $now가 들어가야 하는데 안 들어가있는데? + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); + $row = $db->queryFirstRow( + 'SELECT * FROM `message` WHERE `id` = %i AND valid_until > %i', + $messageID, + $clock->nowTick(), + ); if (!$row) { return null; } @@ -171,12 +175,12 @@ class Message { $db = DB::db(); - $date = (new \DateTime())->format('Y-m-d H:i:s'); + $date = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->nowTick(); $where = new \WhereClause('and'); $where->add('mailbox = %i', $mailbox); $where->add('type = %s', $msgType->value); - $where->add('valid_until > %s', $date); + $where->add('valid_until > %i', $date); if ($fromSeq > 0) { $where->add('id >= %i', $fromSeq); } @@ -203,12 +207,12 @@ class Message { $db = DB::db(); - $date = (new \DateTime())->format('Y-m-d H:i:s'); + $date = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->nowTick(); $where = new \WhereClause('and'); $where->add('mailbox = %i', $mailbox); $where->add('type = %s', $msgType->value); - $where->add('valid_until > %s', $date); + $where->add('valid_until > %i', $date); $where->add('id < %i', $toSeq); if ($limit > 0) { @@ -236,7 +240,8 @@ class Message return '시스템 외교 메시지는 삭제할 수 없습니다.'; } - $prev5min = new \DateTime(); + $clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); + $prev5min = \DateTime::createFromImmutable($clock->tickToDateTime($clock->nowTick())); $prev5min->sub(new \DateInterval('PT5M')); if($msgObj->date < $prev5min){ @@ -265,14 +270,15 @@ class Message } - $in1min = new \DateTime(); + $now = \DateTime::createFromImmutable($clock->tickToDateTime($clock->nowTick())); + $in1min = clone $now; $in1min->add(new \DateInterval('PT1M')); $newMsg = new Message( $msgObj->msgType, $msgObj->src, $msgObj->dest, "req_del_msg", - new \DateTime(), + $now, $in1min, $msgOption ); @@ -300,13 +306,23 @@ class Message $db = DB::db(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); + $timeTick = $clock->nowTick(); + if (Util::toInt($this->validUntil->format('Y')) >= 9000) { + $validUntilTick = GameClock::MAX_SAFE_TICK; + } else { + $validitySeconds = $this->validUntil->getTimestamp() - $this->date->getTimestamp(); + $validUntilTick = $timeTick + $clock->ticksFromSeconds($validitySeconds); + } + $this->date = \DateTime::createFromImmutable($clock->tickToDateTime($timeTick)); + $this->validUntil = \DateTime::createFromImmutable($clock->tickToDateTime($validUntilTick)); $db->insert('message', [ 'mailbox' => $mailbox, 'type' => $this->msgType->value, 'src' => $src_id, 'dest' => $dest_id, - 'time' => $this->date->format('Y-m-d H:i:s'), - 'valid_until' => $this->validUntil->format('Y-m-d H:i:s'), + 'time' => $timeTick, + 'valid_until' => $validUntilTick, 'message' => Json::encode([ 'src'=>($this->src)?($this->src->toArray()):[], 'dest'=>($this->dest)?($this->dest->toArray()):[], @@ -489,7 +505,8 @@ class Message 'text' => $this->msg, 'option' => $this->msgOption ]), - 'valid_until'=>$this->validUntil->format('Y-m-d H:i:s'), + 'valid_until'=>GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')) + ->dateTimeToTick($this->validUntil), ], 'id=%i', $this->id); } diff --git a/hwe/sammo/ResetHelper.php b/hwe/sammo/ResetHelper.php index 3842718f..905c6d2f 100644 --- a/hwe/sammo/ResetHelper.php +++ b/hwe/sammo/ResetHelper.php @@ -245,21 +245,35 @@ class ResetHelper{ true ); - if($sync == 0) { - // 현재 시간을 1월로 맞춤 - $starttime = cutTurn($turntime, $turnterm); - $month = 1; - $year = $startyear; - } else { - // 현재 시간과 동기화 - [$starttime, $yearPulled, $month] = cutDay($turntime, $turnterm); + $requestedTime = new \DateTimeImmutable($turntime); + if($sync == 0) { + // 현재 시간을 1월로 맞춤 + $baseTime = new \DateTimeImmutable(cutTurnDateTime($turntime, $turnterm)); + $month = 1; + $year = $startyear; + } else { + // 현재 시간과 동기화 + [$baseTimeString, $yearPulled, $month] = cutDay($turntime, $turnterm); + $baseTime = new \DateTimeImmutable($baseTimeString); if($yearPulled){ $year = $startyear-1; } else{ $year = $startyear; } - } + } + + $wallNow = GameClock::readWallTime(); + $initialClock = new GameClock( + $baseTime, + $turnterm, + 0, + GameClock::MODE_REALTIME, + $wallNow, + fn (): \DateTimeImmutable => $wallNow, + ); + $requestedTick = $initialClock->dateTimeToTick($requestedTime); + $currentTick = $initialClock->dateTimeToTick($wallNow); $killturn = 4800 / $turnterm; if($npcmode == 1) { $killturn = intdiv($killturn, 3); } @@ -283,10 +297,14 @@ class ResetHelper{ 'maxnation'=>GameConst::$defaultMaxNation, 'refreshLimit'=>30000, 'develcost'=>$develcost, - 'turntime'=>$turntime, - 'starttime'=>$starttime, - 'opentime'=>$turntime, - 'turnterm'=>$turnterm, + 'turntime'=>$requestedTick, + 'starttime'=>0, + 'opentime'=>$requestedTick, + 'turnterm'=>$turnterm, + 'clock_base_time'=>TimeUtil::format($baseTime, true), + 'clock_tick'=>$currentTick, + 'clock_mode'=>GameClock::MODE_REALTIME, + 'clock_wall_anchor'=>TimeUtil::format($wallNow, true), 'killturn'=>$killturn, 'genius'=>GameConst::$defaultMaxGenius, 'show_img_level'=>$show_img_level, @@ -308,7 +326,7 @@ class ResetHelper{ 'name'=>$admin['name'], 'picture'=>$admin['picture'], 'imgsvr'=>$admin['imgsvr'], - 'turntime'=>$turntime, + 'turntime'=>$requestedTick, 'killturn'=>9999, 'crewtype'=>GameUnitConst::DEFAULT_CREWTYPE ]); @@ -344,7 +362,7 @@ class ResetHelper{ $db->insert('ng_games', [ 'server_id'=>$serverID, - 'date'=>$turntime, + 'date'=>TimeUtil::format($requestedTime, false), 'winner_nation'=>null, 'map'=>$scenarioObj->getMapTheme(), 'season'=>$seasonIdx, @@ -368,4 +386,4 @@ class ResetHelper{ 'result'=>true ]; } -} \ No newline at end of file +} diff --git a/hwe/sammo/Scenario/GeneralBuilder.php b/hwe/sammo/Scenario/GeneralBuilder.php index 31ddb4a1..83d6b626 100644 --- a/hwe/sammo/Scenario/GeneralBuilder.php +++ b/hwe/sammo/Scenario/GeneralBuilder.php @@ -653,7 +653,7 @@ class GeneralBuilder{ $officerLevel = $nationID?1:0; } - $turntime = \sammo\getRandTurn($this->rng, $env['turnterm'], new \DateTimeImmutable($env['turntime'])); + $turntime = \sammo\getRandTurn($this->rng, $env['turnterm'], Util::toInt($env['turntime'])); if($this->killturn){ $killturn = $this->killturn; @@ -735,4 +735,4 @@ class GeneralBuilder{ return true; //생성되었다. } -} \ No newline at end of file +} diff --git a/hwe/sammo/ScoutMessage.php b/hwe/sammo/ScoutMessage.php index 6b847d64..e70aff45 100644 --- a/hwe/sammo/ScoutMessage.php +++ b/hwe/sammo/ScoutMessage.php @@ -35,7 +35,7 @@ class ScoutMessage extends Message $this->validScout = false; } - if ($this->validUntil <= new \DateTime()) { + if ($this->validUntil <= $this->date) { $this->validScout = false; } } @@ -122,11 +122,11 @@ class ScoutMessage extends Message public static function invalidateAll(int $generalID, ?int $exceptMsgID = null) { $db = DB::db(); - $now = TimeUtil::now(); + $now = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->nowTick(); //XXX: 뭔가 기존 쿼리가 애매하다. invalid 관련해서 다른 옵션이 가능한가? $rawMsgList = Util::convertArrayToDict($db->query( 'SELECT * FROM `message` WHERE - `mailbox` = %i AND `type` = "private" AND `dest` = `mailbox` AND `valid_until` > %s AND + `mailbox` = %i AND `type` = "private" AND `dest` = `mailbox` AND `valid_until` > %i AND JSON_VALUE(message, "$.option.action") = %s', $generalID, $now, diff --git a/hwe/sammo/ServerTool.php b/hwe/sammo/ServerTool.php index 3188b3f7..e4451a98 100644 --- a/hwe/sammo/ServerTool.php +++ b/hwe/sammo/ServerTool.php @@ -40,34 +40,22 @@ final class ServerTool $locked = tryLock(); } - $oldunit = $admin['turnterm'] * 60; - $unit = $turnterm * 60; - - if($unit == $oldunit){ + if($turnterm == $admin['turnterm']){ if($locked){ unlock(); } return null; } - $unitDiff = $unit / $oldunit; - - $servTurnTime = new \DateTimeImmutable($admin['turntime']); - foreach ($db->query('SELECT no,turntime FROM general') as $gen) { - $genTurnTime = new \DateTimeImmutable($gen['turntime']); - $timeDiff = TimeUtil::DateIntervalToSeconds($genTurnTime->diff($servTurnTime)); - $timeDiff *= $unitDiff; - $newGenTurnTime = $servTurnTime->add(TimeUtil::secondsToDateInterval($timeDiff)); - - $db->update('general', [ - 'turntime' => $newGenTurnTime->format('Y-m-d H:i:s.u') - ], 'no=%i', $gen['no']); - } - $turn = ($admin['year'] - $admin['startyear']) * 12 + $admin['month'] - 1; - $starttime = $servTurnTime->sub(TimeUtil::secondsToDateInterval($turn * $unit))->format('Y-m-d H:i:s'); - $starttime = cutTurn($starttime, $turnterm, false); + $oldClock = GameClock::fromStorage($gameStor); + $currentTick = $oldClock->nowTick(); + $currentDisplay = $oldClock->tickToDateTime($currentTick); + $oldClock->persistTick($gameStor, $currentTick); $gameStor->turnterm = $turnterm; - $gameStor->starttime = $starttime; + $gameStor->clock_base_time = TimeUtil::format( + GameClock::baseTimeForProjection($currentDisplay, $currentTick, $turnterm), + true, + ); pushGlobalHistoryLog(["★턴시간이 {$turnterm}분으로 변경됩니다."]); if($locked){ diff --git a/hwe/sammo/TurnExecutionHelper.php b/hwe/sammo/TurnExecutionHelper.php index 5a4a98a8..761af2ef 100644 --- a/hwe/sammo/TurnExecutionHelper.php +++ b/hwe/sammo/TurnExecutionHelper.php @@ -215,14 +215,13 @@ class TurnExecutionHelper $general->rebirth(); } - $turntime = addTurn($general->getTurnTime(), $gameStor->turnterm); + $turntime = addTurn($general->getTurnTick(), $gameStor->turnterm); $nextTurnTimeBase = $general->getAuxVar('nextTurnTimeBase'); if($nextTurnTimeBase !== null){ $turntime = cutTurn($turntime, $gameStor->turnterm); - $turntimeObj = new \DateTimeImmutable($turntime); - $turntimeObj = $turntimeObj->add(TimeUtil::secondsToDateInterval($nextTurnTimeBase)); - $turntime = TimeUtil::format($turntimeObj, true); + $clock = GameClock::fromStorage($gameStor); + $turntime += $clock->ticksFromSeconds($nextTurnTimeBase); $general->setAuxVar('nextTurnTimeBase', null); } @@ -230,7 +229,7 @@ class TurnExecutionHelper } - static public function executeGeneralCommandUntil(string $date, \DateTimeInterface $limitActionTime, int $year, int $month) + static public function executeGeneralCommandUntil(int $date, \DateTimeInterface $limitActionTime, int $year, int $month) { $db = DB::db(); $generalsTodo = $db->query( @@ -244,7 +243,7 @@ class TurnExecutionHelper $autorun_user = $gameStor->autorun_user; foreach ($generalsTodo as $rawGeneral) { - $currActionTime = new \DateTimeImmutable(); + $currActionTime = GameClock::readWallTime(); if ($currActionTime > $limitActionTime) { return [true, $currentTurn]; } @@ -350,7 +349,7 @@ class TurnExecutionHelper pullNationCommand($general->getVar('nation'), $general->getVar('officer_level')); pullGeneralCommand($general->getID()); - $currentTurn = $general->getTurnTime(); + $currentTurn = $general->getTurnTick(); $general->increaseVarWithLimit('myset', GameConst::$incDefSettingChange, null, GameConst::$maxDefSettingChange); if (($autorun_user['limit_minutes'] ?? false) && $general->getNPCType() < 2 && $hasReservedTurn) { @@ -390,13 +389,14 @@ class TurnExecutionHelper return true; } - static public function executeAllCommand(&$executed = false, &$locked = false): string + static public function executeAllCommand(&$executed = false, &$locked = false): int { $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); - if (TimeUtil::now(true) < $gameStor->turntime) { + $clock = GameClock::fromStorage($gameStor); + if ($clock->nowTick() < $gameStor->turntime) { //턴 시각 이전이면 아무것도 하지 않음 return $gameStor->turntime; } @@ -420,7 +420,7 @@ class TurnExecutionHelper //접속자 수 따라서 갱신제한 변경 CheckOverhead(); - $date = TimeUtil::now(true); + $date = $clock->nowTick(); // 최종 처리 월턴의 다음 월턴시간 구함 //$lastExecuted = $gameStor->turntime; $prevTurn = cutTurn($gameStor->turntime, $gameStor->turnterm); @@ -433,7 +433,7 @@ class TurnExecutionHelper $maxActionTime = max($maxActionTime * 2 / 3, $maxActionTime - 10); } - $limitActionTime = (new \DateTimeImmutable())->add(TimeUtil::secondsToDateInterval($maxActionTime)); + $limitActionTime = GameClock::readWallTime()->add(TimeUtil::secondsToDateInterval($maxActionTime)); // 현재 턴 이전 월턴까지 모두처리. //최종 처리 이후 다음 월턴이 현재 시간보다 전이라면 diff --git a/hwe/sammo/WarUnitGeneral.php b/hwe/sammo/WarUnitGeneral.php index c9bb5e16..30cd47b6 100644 --- a/hwe/sammo/WarUnitGeneral.php +++ b/hwe/sammo/WarUnitGeneral.php @@ -57,16 +57,16 @@ class WarUnitGeneral extends WarUnit $this->general->increaseRankVar(RankColumn::warnum, 1); if ($this->isAttacker) { - $semiTurn = $general->getTurnTime(); + $semiTurn = $general->getTurnTick(); } else if ($oppose !== null) { - $semiTurn = $oppose->getGeneral()->getTurnTime(); + $semiTurn = $oppose->getGeneral()->getTurnTick(); } else { LogText("WarUnitGeneral::setOppose", "defender인데 oppose가 null {$general->getID()}, {$general->getTurnTime()}"); - $semiTurn = $general->getTurnTime(); + $semiTurn = $general->getTurnTick(); } $phase = $this->getRealPhase(); - $semiTurn = substr($semiTurn, 0, strlen($semiTurn) - 2); - $semiTurn .= sprintf("%02d", Util::valueFit($phase, 0, 99)); + $semiTurn -= $semiTurn % 100; + $semiTurn += Util::valueFit($phase, 0, 99); $general->setVar('recent_war', $semiTurn); } diff --git a/hwe/sql/schema.sql b/hwe/sql/schema.sql index ebe4ae75..db095f46 100644 --- a/hwe/sql/schema.sql +++ b/hwe/sql/schema.sql @@ -45,8 +45,8 @@ CREATE TABLE `general` ( `book` VARCHAR(20) NOT NULL DEFAULT 'None', `horse` VARCHAR(20) NOT NULL DEFAULT 'None', `item` VARCHAR(20) NOT NULL DEFAULT 'None', - `turntime` DATETIME(6) NOT NULL, - `recent_war` DATETIME(6) NULL DEFAULT NULL, + `turntime` BIGINT NOT NULL, + `recent_war` BIGINT NULL DEFAULT NULL, `makelimit` INT(2) NULL DEFAULT '0', `killturn` INT(3) NULL DEFAULT NULL, `block` INT(1) NULL DEFAULT '0', @@ -97,7 +97,7 @@ CREATE TABLE `general_access_log` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `general_id` INT(11) NOT NULL, `user_id` INT(11) NULL DEFAULT NULL, - `last_refresh` DATETIME NULL DEFAULT NULL, + `last_refresh` BIGINT NULL DEFAULT NULL, `refresh` INT(11) NOT NULL DEFAULT '0', `refresh_total` INT(11) NOT NULL DEFAULT '0', `refresh_score` INT(11) NOT NULL DEFAULT '0', @@ -244,8 +244,8 @@ CREATE TABLE `message` ( `type` ENUM('private', 'national', 'public', 'diplomacy') NOT NULL, `src` INT(11) NOT NULL, `dest` INT(11) NOT NULL, - `time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - `valid_until` DATETIME NOT NULL DEFAULT '9999-12-31 23:59:59', + `time` BIGINT NOT NULL, + `valid_until` BIGINT NOT NULL, `message` TEXT NOT NULL COLLATE 'utf8mb4_bin', PRIMARY KEY (`id`), INDEX `by_mailbox` (`mailbox`, `type`, `id`), @@ -536,8 +536,8 @@ CREATE TABLE IF NOT EXISTS `reserved_open` ( CREATE TABLE `select_npc_token` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `owner` INT(11) NOT NULL, - `valid_until` DATETIME NOT NULL, - `pick_more_from` DATETIME NOT NULL, + `valid_until` BIGINT NOT NULL, + `pick_more_from` BIGINT NOT NULL, `pick_result` TEXT NOT NULL COLLATE 'utf8mb4_bin', `nonce` INT(11) NOT NULL, PRIMARY KEY (`id`), @@ -553,7 +553,7 @@ CREATE TABLE `select_pool` ( `unique_name` VARCHAR(20) NOT NULL, `owner` INT(11) NULL DEFAULT NULL, `general_id` INT(11) NULL DEFAULT NULL, - `reserved_until` DATETIME NULL DEFAULT NULL, + `reserved_until` BIGINT NULL DEFAULT NULL, `info` TEXT NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `unique_name` (`unique_name`), @@ -694,11 +694,11 @@ CREATE TABLE `ng_auction` ( `target` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_bin', `host_general_id` INT(11) NOT NULL, `req_resource` ENUM('gold','rice','inheritPoint') NOT NULL COLLATE 'utf8mb4_bin', - `open_date` DATETIME NOT NULL, - `close_date` DATETIME NOT NULL, + `open_tick` BIGINT NOT NULL, + `close_tick` BIGINT NOT NULL, `detail` LONGTEXT NOT NULL COLLATE 'utf8mb4_bin', PRIMARY KEY (`id`) USING BTREE, - INDEX `by_close` (`finished`, `type`, `close_date`) USING BTREE, + INDEX `by_close` (`finished`, `type`, `close_tick`) USING BTREE, INDEX `by_general_id` (`host_general_id`, `type`, `finished`) USING BTREE, CONSTRAINT `detail` CHECK (json_valid(`detail`)) ) @@ -721,4 +721,4 @@ CREATE TABLE `ng_auction_bid` ( CONSTRAINT `aux` CHECK (json_valid(`aux`)) ) COLLATE='utf8mb4_general_ci' -ENGINE = Aria; \ No newline at end of file +ENGINE = Aria; diff --git a/scripts/README-game-clock.md b/scripts/README-game-clock.md new file mode 100644 index 00000000..729f8177 --- /dev/null +++ b/scripts/README-game-clock.md @@ -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을 넘지 않았는지 검사합니다. diff --git a/scripts/game-clock.php b/scripts/game-clock.php new file mode 100644 index 00000000..fb6d8b13 --- /dev/null +++ b/scripts/game-clock.php @@ -0,0 +1,129 @@ +#!/usr/bin/env php +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); diff --git a/scripts/migrate-game-clock.php b/scripts/migrate-game-clock.php new file mode 100644 index 00000000..fcca414d --- /dev/null +++ b/scripts/migrate-game-clock.php @@ -0,0 +1,340 @@ +#!/usr/bin/env php + */ +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>} */ +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"); diff --git a/scripts/verify-game-clock-engine.php b/scripts/verify-game-clock-engine.php new file mode 100644 index 00000000..432b76a3 --- /dev/null +++ b/scripts/verify-game-clock-engine.php @@ -0,0 +1,76 @@ +#!/usr/bin/env php + 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']), +); diff --git a/src/sammo/GameClock.php b/src/sammo/GameClock.php new file mode 100644 index 00000000..0103ff67 --- /dev/null +++ b/src/sammo/GameClock.php @@ -0,0 +1,284 @@ +wallNowProvider = $wallNowProvider; + } + + public static function fromStorage(KVStorage $gameStor, ?callable $wallNowProvider = null): self + { + $values = $gameStor->getValues([ + 'clock_base_time', + 'clock_tick', + 'clock_mode', + 'clock_wall_anchor', + 'turnterm', + ]); + + $baseTime = new \DateTimeImmutable((string)$values['clock_base_time']); + $wallAnchor = new \DateTimeImmutable((string)$values['clock_wall_anchor']); + + return new self( + $baseTime, + Util::toInt($values['turnterm']), + Util::toInt($values['clock_tick']), + (string)$values['clock_mode'], + $wallAnchor, + $wallNowProvider, + ); + } + + public static function initializeStorage( + KVStorage $gameStor, + \DateTimeInterface $baseTime, + int $turnTermMinutes, + int $currentTick, + string $mode = self::MODE_REALTIME, + ?\DateTimeInterface $wallAnchor = null, + ): void { + $wallAnchor ??= self::readWallTime(); + // 생성자 validation을 초기화 경로에도 동일하게 적용합니다. + new self( + \DateTimeImmutable::createFromInterface($baseTime), + $turnTermMinutes, + $currentTick, + $mode, + \DateTimeImmutable::createFromInterface($wallAnchor), + ); + + $gameStor->clock_base_time = TimeUtil::format($baseTime, true); + $gameStor->clock_tick = $currentTick; + $gameStor->clock_mode = $mode; + $gameStor->clock_wall_anchor = TimeUtil::format($wallAnchor, true); + } + + public function getBaseTime(): \DateTimeImmutable + { + return $this->baseTime; + } + + public function getTurnTermMinutes(): int + { + return $this->turnTermMinutes; + } + + public function getMode(): string + { + return $this->mode; + } + + public function ticksPerSecond(): int + { + return intdiv(self::TICKS_PER_TURN, $this->turnTermMinutes * 60); + } + + public function nowTick(): int + { + if ($this->mode === self::MODE_MANUAL) { + return $this->anchorTick; + } + + return self::addTicks($this->anchorTick, $this->ticksBetween($this->wallAnchor, $this->wallNow())); + } + + public function ticksFromSeconds(int|float $seconds): int + { + if (is_int($seconds)) { + return $seconds * $this->ticksPerSecond(); + } + return (int)round($seconds * $this->ticksPerSecond()); + } + + public function ticksFromMinutes(int|float $minutes): int + { + return $this->ticksFromSeconds($minutes * 60); + } + + public function addTurns(int $tick, int $turns = 1): int + { + if (abs($turns) > intdiv(self::MAX_SAFE_TICK, self::TICKS_PER_TURN)) { + throw new \OverflowException('turn 수가 JavaScript safe integer tick 범위를 벗어났습니다.'); + } + return self::addTicks($tick, self::TICKS_PER_TURN * $turns); + } + + public static function addTicks(int $tick, int $deltaTick): int + { + self::requireSafeTick($tick); + if ($deltaTick > self::MAX_SAFE_TICK || $deltaTick < -self::MAX_SAFE_TICK) { + throw new \OverflowException('delta tick이 JavaScript safe integer 범위를 벗어났습니다.'); + } + if (($deltaTick > 0 && $tick > self::MAX_SAFE_TICK - $deltaTick) + || ($deltaTick < 0 && $tick < -self::MAX_SAFE_TICK - $deltaTick)) { + throw new \OverflowException('JavaScript safe integer tick 범위를 벗어났습니다.'); + } + return self::requireSafeTick($tick + $deltaTick); + } + + public static function requireSafeTick(int $tick): int + { + if (abs($tick) > self::MAX_SAFE_TICK) { + throw new \OverflowException("tick {$tick}은 JavaScript safe integer 범위를 벗어났습니다."); + } + return $tick; + } + + public function floorTurn(int $tick): int + { + $remainder = $tick % self::TICKS_PER_TURN; + if ($remainder < 0) { + $remainder += self::TICKS_PER_TURN; + } + return $tick - $remainder; + } + + /** @return array{turn:int, subTick:int} */ + public function splitTick(int $tick): array + { + $turnStart = $this->floorTurn($tick); + return [ + 'turn' => intdiv($turnStart, self::TICKS_PER_TURN), + 'subTick' => $tick - $turnStart, + ]; + } + + public function dateTimeToTick(\DateTimeInterface $dateTime): int + { + return self::requireSafeTick($this->ticksBetween($this->baseTime, $dateTime)); + } + + public function tickToDateTime(int $tick): \DateTimeImmutable + { + self::requireSafeTick($tick); + $ticksPerSecond = $this->ticksPerSecond(); + $seconds = intdiv($tick, $ticksPerSecond); + $remainingTicks = $tick % $ticksPerSecond; + if ($remainingTicks < 0) { + $seconds--; + $remainingTicks += $ticksPerSecond; + } + $microseconds = intdiv($remainingTicks * 1_000_000, $ticksPerSecond); + + $result = $this->baseTime->modify("{$seconds} seconds"); + if ($microseconds !== 0) { + $result = $result->modify("{$microseconds} microseconds"); + } + return $result; + } + + public function formatTick(int $tick, bool $withFraction = false): string + { + return TimeUtil::format($this->tickToDateTime($tick), $withFraction); + } + + public static function baseTimeForProjection( + \DateTimeInterface $projectedTime, + int $tick, + int $turnTermMinutes, + ): \DateTimeImmutable { + $ticksPerSecond = intdiv(self::TICKS_PER_TURN, $turnTermMinutes * 60); + if ($ticksPerSecond <= 0 || self::TICKS_PER_TURN % ($turnTermMinutes * 60) !== 0) { + throw new \InvalidArgumentException('정수 tick/초로 표현할 수 없는 turnterm입니다.'); + } + $seconds = intdiv($tick, $ticksPerSecond); + $remainingTicks = $tick % $ticksPerSecond; + if ($remainingTicks < 0) { + $seconds--; + $remainingTicks += $ticksPerSecond; + } + $microseconds = intdiv($remainingTicks * 1_000_000, $ticksPerSecond); + $negativeSeconds = -$seconds; + $result = \DateTimeImmutable::createFromInterface($projectedTime)->modify("{$negativeSeconds} seconds"); + if ($microseconds !== 0) { + $negativeMicroseconds = -$microseconds; + $result = $result->modify("{$negativeMicroseconds} microseconds"); + } + return $result; + } + + public function persistTick(KVStorage $gameStor, int $tick, ?string $mode = null): void + { + self::requireSafeTick($tick); + $mode ??= $this->mode; + if (!in_array($mode, [self::MODE_REALTIME, self::MODE_MANUAL], true)) { + throw new \InvalidArgumentException("알 수 없는 game clock mode: {$mode}"); + } + $gameStor->clock_tick = $tick; + $gameStor->clock_mode = $mode; + // 수동 시계의 전진은 실제 시간에 의존하지 않아야 합니다. realtime으로 + // 전환하는 순간에만 새 벽시계 anchor를 읽습니다. + $wallAnchor = $mode === self::MODE_REALTIME ? $this->wallNow() : $this->wallAnchor; + $gameStor->clock_wall_anchor = TimeUtil::format($wallAnchor, true); + } + + public function advance(KVStorage $gameStor, int $deltaTick): int + { + $nextTick = $this->nowTick() + $deltaTick; + $this->persistTick($gameStor, $nextTick); + return $nextTick; + } + + private function ticksBetween(\DateTimeInterface $from, \DateTimeInterface $to): int + { + $secondDiff = $to->getTimestamp() - $from->getTimestamp(); + $microsecondDiff = Util::toInt($to->format('u')) - Util::toInt($from->format('u')); + $ticksPerSecond = $this->ticksPerSecond(); + + return $secondDiff * $ticksPerSecond + + intdiv($microsecondDiff * $ticksPerSecond, 1_000_000); + } + + private function wallNow(): \DateTimeImmutable + { + if ($this->wallNowProvider !== null) { + $now = ($this->wallNowProvider)(); + if (!$now instanceof \DateTimeImmutable) { + throw new \UnexpectedValueException('wallNowProvider는 DateTimeImmutable을 반환해야 합니다.'); + } + return $now; + } + return self::readWallTime(); + } + + /** 운영 anchor와 실행 budget에서만 사용하는 실제 벽시계 입력입니다. */ + public static function readWallTime(): \DateTimeImmutable + { + return new \DateTimeImmutable(); + } +} diff --git a/tests/GameClockBoundaryTest.php b/tests/GameClockBoundaryTest.php new file mode 100644 index 00000000..033cfb00 --- /dev/null +++ b/tests/GameClockBoundaryTest.php @@ -0,0 +1,59 @@ + [$path], [ + 'hwe/sammo/TurnExecutionHelper.php', + 'hwe/sammo/Auction.php', + 'hwe/sammo/AuctionBasicResource.php', + 'hwe/sammo/AuctionUniqueItem.php', + 'hwe/func_auction.php', + 'hwe/func_tournament.php', + 'hwe/c_tournament.php', + 'hwe/sammo/AbsFromUserPool.php', + 'hwe/sammo/GeneralPool/RandomNameGeneral.php', + 'hwe/sammo/API/General/DieOnPrestart.php', + ]); + } + + public function testTickSchemaDoesNotUseDatabaseDefaultsForGameSchedules(): void + { + $schema = file_get_contents(__DIR__ . '/../hwe/sql/schema.sql'); + self::assertIsString($schema); + foreach ([ + '`turntime` BIGINT', + '`recent_war` BIGINT', + '`last_refresh` BIGINT', + '`time` BIGINT', + '`valid_until` BIGINT', + '`reserved_until` BIGINT', + '`open_tick` BIGINT', + '`close_tick` BIGINT', + ] as $expected) { + self::assertStringContainsString($expected, $schema); + } + } +} diff --git a/tests/GameClockTest.php b/tests/GameClockTest.php new file mode 100644 index 00000000..74a08208 --- /dev/null +++ b/tests/GameClockTest.php @@ -0,0 +1,99 @@ +ticksPerSecond()); + self::assertSame(GameClock::TICKS_PER_TURN, $clock->ticksFromMinutes($turnTerm)); + } + + public static function supportedTurnTerms(): array + { + return [ + '1 minute' => [1, 600_000], + '2 minutes' => [2, 300_000], + '5 minutes' => [5, 120_000], + '10 minutes' => [10, 60_000], + '60 minutes' => [60, 10_000], + '120 minutes' => [120, 5_000], + ]; + } + + public function testTickFormulaAndDisplayProjectionRoundTrip(): void + { + $base = new \DateTimeImmutable('2026-08-03 12:34:56.000000'); + $clock = new GameClock($base, 60, 0, GameClock::MODE_MANUAL, $base); + $tick = GameClock::TICKS_PER_TURN * 7 + 12_345; + + self::assertSame(['turn' => 7, 'subTick' => 12_345], $clock->splitTick($tick)); + self::assertSame($tick, $clock->dateTimeToTick($clock->tickToDateTime($tick))); + self::assertSame('2026-08-03 19:34:57.234500', $clock->formatTick($tick, true)); + } + + public function testManualModeDoesNotReadWallClock(): void + { + $base = new \DateTimeImmutable('2026-08-03 00:00:00.000000'); + $wallRead = false; + $clock = new GameClock( + $base, + 10, + 123_456, + GameClock::MODE_MANUAL, + $base, + function () use (&$wallRead): \DateTimeImmutable { + $wallRead = true; + return new \DateTimeImmutable('2099-01-01 00:00:00.000000'); + }, + ); + + self::assertSame(123_456, $clock->nowTick()); + self::assertFalse($wallRead); + } + + public function testNegativeTickProjectionAndBaseRecalculation(): void + { + $projected = new \DateTimeImmutable('2026-08-03 12:00:00.123400'); + $tick = -36_001_234; + $base = GameClock::baseTimeForProjection($projected, $tick, 60); + $clock = new GameClock($base, 60, $tick, GameClock::MODE_MANUAL, $projected); + + self::assertSame($tick, $clock->dateTimeToTick($clock->tickToDateTime($tick))); + self::assertSame('2026-08-03 12:00:00.123400', $clock->formatTick($tick, true)); + self::assertSame(['turn' => -2, 'subTick' => 35_998_766], $clock->splitTick($tick)); + } + + public function testRealtimeModeAdvancesFromAnchorWithoutDatabaseNow(): void + { + $base = new \DateTimeImmutable('2026-08-03 00:00:00.000000'); + $wallAnchor = new \DateTimeImmutable('2026-08-03 10:00:00.000000'); + $clock = new GameClock( + $base, + 120, + 100, + GameClock::MODE_REALTIME, + $wallAnchor, + fn (): \DateTimeImmutable => new \DateTimeImmutable('2026-08-03 10:00:01.500000'), + ); + + self::assertSame(7_600, $clock->nowTick()); + } + + public function testTickArithmeticRejectsValuesThatJavaScriptCannotRepresentExactly(): void + { + self::assertSame(GameClock::MAX_SAFE_TICK, GameClock::addTicks(GameClock::MAX_SAFE_TICK - 1, 1)); + + $this->expectException(\OverflowException::class); + GameClock::addTicks(GameClock::MAX_SAFE_TICK, 1); + } +}