diff --git a/hwe/_admin1_submit.php b/hwe/_admin1_submit.php index 51827d42..47732316 100644 --- a/hwe/_admin1_submit.php +++ b/hwe/_admin1_submit.php @@ -25,13 +25,15 @@ if (!$v->validate()) { $msg = Util::getPost('msg'); $btn = Util::getPost('btn'); $log = Util::getPost('log'); -$starttime = Util::getPost('starttime', 'string', (new \DateTime())->format('Y-m-d H:i:s')); +$starttime = Util::getPost('starttime', 'string', null); $maxgeneral = Util::getPost('maxgeneral', 'int', GameConst::$defaultMaxGeneral); $maxnation = Util::getPost('maxnation', 'int', GameConst::$defaultMaxNation); $startyear = Util::getPost('startyear', 'int', GameConst::$defaultStartYear); $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); +$clock = GameClock::fromStorage($gameStor); +$starttime ??= $clock->formatTick($clock->nowTick()); $admin = getAdmin(); @@ -43,7 +45,6 @@ switch ($btn) { pushGlobalHistoryLog(["{$log}"]); break; case "변경1": - $clock = GameClock::fromStorage($gameStor); $gameStor->clock_base_time = TimeUtil::format(GameClock::baseTimeForProjection( new \DateTimeImmutable($starttime), Util::toInt($gameStor->starttime), diff --git a/hwe/_admin2_submit.php b/hwe/_admin2_submit.php index e3ad0652..d2502089 100644 --- a/hwe/_admin2_submit.php +++ b/hwe/_admin2_submit.php @@ -113,7 +113,7 @@ switch ($btn) { case "경험치1000": $text = $btn . " 지급!"; foreach ($genlist as $generalID) { - $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []); + $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg->send(true); } $db->update('general', [ @@ -124,7 +124,7 @@ switch ($btn) { case "공헌치1000": $text = $btn . " 지급!"; foreach ($genlist as $generalID) { - $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []); + $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg->send(true); } $db->update('general', [ @@ -135,7 +135,7 @@ switch ($btn) { case "보숙10000": $text = "보병숙련도+10000 지급!"; foreach ($genlist as $generalID) { - $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []); + $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg->send(true); } $db->update('general', [ @@ -145,7 +145,7 @@ switch ($btn) { case "궁숙10000": $text = "궁병숙련도+10000 지급!"; foreach ($genlist as $generalID) { - $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []); + $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg->send(true); } $db->update('general', [ @@ -156,7 +156,7 @@ switch ($btn) { $src = MessageTarget::buildQuick($session->generalID); $text = "기병숙련도+10000 지급!"; foreach ($genlist as $generalID) { - $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []); + $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg->send(true); } $db->update('general', [ @@ -167,7 +167,7 @@ switch ($btn) { $src = MessageTarget::buildQuick($session->generalID); $text = "귀병숙련도+10000 지급!"; foreach ($genlist as $generalID) { - $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []); + $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg->send(true); } $db->update('general', [ @@ -178,7 +178,7 @@ switch ($btn) { $src = MessageTarget::buildQuick($session->generalID); $text = "차병숙련도+10000 지급!"; foreach ($genlist as $generalID) { - $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []); + $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg->send(true); } $db->update('general', [ @@ -198,7 +198,7 @@ switch ($btn) { case "메세지 전달": $text = $msg ?? ''; foreach ($genlist as $generalID) { - $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []); + $msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg->send(true); } break; diff --git a/hwe/_admin8.php b/hwe/_admin8.php index a70018b4..8248b54d 100644 --- a/hwe/_admin8.php +++ b/hwe/_admin8.php @@ -100,8 +100,6 @@ $sel[$type] = "selected"; throw new \Exception("알 수 없는 외교 상태: {$dip['state']}"); } - $date = TimeUtil::now(); - echo " $nationName[$me] @@ -122,4 +120,4 @@ $sel[$type] = "selected"; - \ No newline at end of file + diff --git a/hwe/a_traffic.php b/hwe/a_traffic.php index 483e061e..655020b3 100644 --- a/hwe/a_traffic.php +++ b/hwe/a_traffic.php @@ -9,6 +9,7 @@ include "func.php"; $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); +$clock = GameClock::fromStorage($gameStor); increaseRefresh("갱신정보", 1); @@ -23,7 +24,7 @@ $recentTraffic[] = [ 'month' => $admin['month'], 'refresh' => $admin['refresh'], 'online' => $curonline, - 'date' => TimeUtil::now() + 'date' => $clock->formatNow() ]; if ($admin['maxrefresh'] == 0) { diff --git a/hwe/func.php b/hwe/func.php index d72cad9b..f3ca231d 100644 --- a/hwe/func.php +++ b/hwe/func.php @@ -1082,12 +1082,12 @@ function updateTraffic() if(count($recentTraffic) >= 5){ array_shift($recentTraffic); } - $recentTraffic[] = [ + $recentTraffic[] = [ 'year'=>$admin['year'], 'month' => $admin['month'], 'refresh' => $admin['refresh'], 'online' => $online, - 'date' => TimeUtil::now(), + 'date' => GameClock::fromStorage($gameStor)->formatNow(), ]; $gameStor->recentTraffic = $recentTraffic; @@ -1376,12 +1376,14 @@ function CheckHall($no) return; } - $unitedDate = TimeUtil::now(); + $clock = GameClock::fromStorage($gameStor); + $unitedDate = $clock->formatTick($clock->nowTick()); $nation = $generalObj->getStaticNation(); $serverCnt = $db->queryFirstField('SELECT count(*) FROM ng_games'); - [$scenarioIdx, $scenarioName, $startTime] = $gameStor->getValuesAsArray(['scenario', 'scenario_text', 'starttime']); + [$scenarioIdx, $scenarioName, $startTick] = $gameStor->getValuesAsArray(['scenario', 'scenario_text', 'starttime']); + $startTime = $clock->formatTick(Util::toInt($startTick)); $ownerName = $generalObj->getVar('owner_name'); if ($generalObj->getVar('owner')) { @@ -1786,11 +1788,13 @@ function deleteNation(General $lord, bool $applyDB): array // 부대 삭제 $db->delete('troop', 'nation=%i', $nationID); - // 국가 삭제 - $db->insert('ng_old_nations', [ - 'server_id' => UniqueConst::$serverID, - 'nation' => $nationID, - 'data' => Json::encode($nation) + // 국가 삭제 + $gameDate = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow(); + $db->insert('ng_old_nations', [ + 'server_id' => UniqueConst::$serverID, + 'nation' => $nationID, + 'date' => $gameDate, + 'data' => Json::encode($nation) ]); $db->delete('nation', 'nation=%i', $nationID); $db->delete('nation_turn', 'nation_id=%i', $nationID); diff --git a/hwe/func_gamerule.php b/hwe/func_gamerule.php index 707d3406..ca448876 100644 --- a/hwe/func_gamerule.php +++ b/hwe/func_gamerule.php @@ -838,20 +838,23 @@ function checkEmperior() $nation['aux'] += $nationStor->max_power ?? []; $nation['history'] = getNationHistoryLogAll($nation['nation']); - storeOldGenerals(0, $admin['year'], $admin['month']); - storeOldGenerals($nation['nation'], $admin['year'], $admin['month']); - - $db->insert('ng_old_nations', [ - 'server_id' => UniqueConst::$serverID, - 'nation' => $nation['nation'], - 'data' => Json::encode($nation) + storeOldGenerals(0, $admin['year'], $admin['month']); + storeOldGenerals($nation['nation'], $admin['year'], $admin['month']); + $gameDate = GameClock::fromStorage($gameStor)->formatNow(); + + $db->insert('ng_old_nations', [ + 'server_id' => UniqueConst::$serverID, + 'nation' => $nation['nation'], + 'date' => $gameDate, + 'data' => Json::encode($nation) ]); $noNationGeneral = $db->queryFirstColumn('SELECT `no` FROM general WHERE nation=0'); $db->insert('ng_old_nations', [ - 'server_id' => UniqueConst::$serverID, - 'nation' => 0, - 'data' => Json::encode([ + 'server_id' => UniqueConst::$serverID, + 'nation' => 0, + 'date' => $gameDate, + 'data' => Json::encode([ 'nation' => 0, 'name' => '재야', 'generals' => $noNationGeneral diff --git a/hwe/index.php b/hwe/index.php index f294d530..15cf758f 100644 --- a/hwe/index.php +++ b/hwe/index.php @@ -7,6 +7,7 @@ include "func.php"; Session::requireLogin()->loginGame()->setReadOnly(); $mapName = GameConst::$mapName; +$frontClock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); ?> @@ -28,7 +29,7 @@ $mapName = GameConst::$mapName; 'maxTurn' => GameConst::$maxTurn, 'maxPushTurn' => 12, - 'serverNow' => TimeUtil::now(false), + 'serverNow' => $frontClock->formatTick($frontClock->nowTick()), ] ], false) ?> @@ -42,4 +43,4 @@ $mapName = GameConst::$mapName;
- \ No newline at end of file + diff --git a/hwe/j_autoreset.php b/hwe/j_autoreset.php index 4a881457..e8ded5c3 100644 --- a/hwe/j_autoreset.php +++ b/hwe/j_autoreset.php @@ -25,21 +25,24 @@ if(!$reserved){ ]); } -$reservedDate = new \DateTime($reserved['date']); -$now = new \DateTime(); +$reservedDate = new \DateTimeImmutable($reserved['date']); +$now = GameClock::readWallTime(); $status = 'not_yet'; -list($isUnited, $lastTurn) = $gameStor->getValuesAsArray(['isunited', 'turntime']); -if($isUnited === null || $lastTurn === null){ - $isUnited = 2; - $lastTurn = '2000-01-01'; -} - -if($lastTurn !== null){ - $lastTurn = new \DateTime($lastTurn); -} +list($isUnited, $unitedWallAnchor) = $gameStor->getValuesAsArray(['isunited', 'autoreset_united_wall_anchor']); +if($isUnited === null){ + $isUnited = 2; +} +$lastTurn = null; +if($isUnited > 0){ + if(!is_string($unitedWallAnchor) || $unitedWallAnchor === ''){ + $unitedWallAnchor = TimeUtil::format($now, true); + $gameStor->autoreset_united_wall_anchor = $unitedWallAnchor; + } + $lastTurn = new \DateTimeImmutable($unitedWallAnchor); +} if($lastTurn === null){ //이미 리셋된 상태임 @@ -99,4 +102,4 @@ $result['affected']=1; $prefix = DB::prefix(); ServConfig::getServerList()[$prefix]->openServer(); -Json::die($result); \ No newline at end of file +Json::die($result); diff --git a/hwe/j_board_article_add.php b/hwe/j_board_article_add.php index 35ea3ae6..be901f1c 100644 --- a/hwe/j_board_article_add.php +++ b/hwe/j_board_article_add.php @@ -10,6 +10,7 @@ $session = Session::requireGameLogin()->setReadOnly(); $userID = Session::getUserID(); $db = DB::db(); +$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); $isSecretBoard = Util::getPost('isSecret', 'bool', false); $title = Util::getPost('title'); @@ -67,7 +68,7 @@ $icon = GetImageURL($me['imgsvr'], $me['picture']); $db->insert('board', [ 'nation_no'=>$me['nation'], 'is_secret'=>$isSecretBoard, - 'date'=>TimeUtil::now(), + 'date'=>$clock->formatNow(), 'general_no'=>$me['no'], 'author'=>$me['name'], 'author_icon'=>$icon, @@ -79,4 +80,4 @@ Json::die([ 'result'=>true, 'reason'=>'success', 'row_id'=>$db->insertId() -]); \ No newline at end of file +]); diff --git a/hwe/j_board_comment_add.php b/hwe/j_board_comment_add.php index 4fc660be..57c41d51 100644 --- a/hwe/j_board_comment_add.php +++ b/hwe/j_board_comment_add.php @@ -10,6 +10,7 @@ $session = Session::requireGameLogin()->setReadOnly(); $userID = Session::getUserID(); $db = DB::db(); +$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); $articleNo = Util::getPost('articleNo', 'int'); $text = Util::getPost('text'); @@ -73,7 +74,7 @@ else if ($isSecretBoard && $permission < 2) { $db->insert('comment', [ 'nation_no'=>$me['nation'], 'is_secret'=>$isSecretBoard, - 'date'=>TimeUtil::now(), + 'date'=>$clock->formatNow(), 'document_no'=>$articleNo, 'general_no'=>$me['no'], 'author'=>$me['name'], @@ -84,4 +85,4 @@ Json::die([ 'result'=>true, 'reason'=>'success', 'row_id'=>$db->insertId() -]); \ No newline at end of file +]); diff --git a/hwe/j_diplomacy_destroy_letter.php b/hwe/j_diplomacy_destroy_letter.php index f7ed149e..dbca577a 100644 --- a/hwe/j_diplomacy_destroy_letter.php +++ b/hwe/j_diplomacy_destroy_letter.php @@ -83,7 +83,7 @@ else{ } -$now = new \DateTime(); +$now = Message::gameNow(); $unlimited = new \DateTime('9999-12-31'); if(in_array($stateOpt, ['try_destroy_src', 'try_destroy_dest'])){ @@ -138,4 +138,4 @@ Json::die([ 'result'=>true, 'reason'=>'success', 'state'=>$lastState -]); \ No newline at end of file +]); diff --git a/hwe/j_diplomacy_respond_letter.php b/hwe/j_diplomacy_respond_letter.php index ce8c4d8f..06b9d455 100644 --- a/hwe/j_diplomacy_respond_letter.php +++ b/hwe/j_diplomacy_respond_letter.php @@ -69,7 +69,7 @@ $destNation = getNationStaticInfo($letter['dest_nation_id']); $src = new MessageTarget($me['no'], $me['name'], $destNation['nation'], $destNation['name'], $destNation['color'], $me['icon']); $dest = new MessageTarget(0, '', $srcNation['nation'], $srcNation['name'], $srcNation['color']); -$now = new \DateTime(); +$now = Message::gameNow(); $unlimited = new \DateTime('9999-12-31'); if($isAgree){ @@ -133,4 +133,4 @@ $msgID = $msg->send(); Json::die([ 'result'=>true, 'reason'=>'success' -]); \ No newline at end of file +]); diff --git a/hwe/j_diplomacy_rollback_letter.php b/hwe/j_diplomacy_rollback_letter.php index f8bc1f9b..951bdbc9 100644 --- a/hwe/j_diplomacy_rollback_letter.php +++ b/hwe/j_diplomacy_rollback_letter.php @@ -63,7 +63,7 @@ $destNation = getNationStaticInfo($letter['dest_nation_id']); $src = new MessageTarget($me['no'], $me['name'], $srcNation['nation'], $srcNation['name'], $srcNation['color'], $me['icon']); $dest = new MessageTarget(0, '', $destNation['nation'], $destNation['name'], $destNation['color']); -$now = new \DateTime(); +$now = Message::gameNow(); $unlimited = new \DateTime('9999-12-31'); $aux['reason'] = [ @@ -91,4 +91,4 @@ $msgID = $msg->send(); Json::die([ 'result'=>true, 'reason'=>'success' -]); \ No newline at end of file +]); diff --git a/hwe/j_diplomacy_send_letter.php b/hwe/j_diplomacy_send_letter.php index ea05671e..f3e9aa85 100644 --- a/hwe/j_diplomacy_send_letter.php +++ b/hwe/j_diplomacy_send_letter.php @@ -140,6 +140,8 @@ else{ $me['icon'] = GetImageURL($me['imgsvr'], $me['picture']); +$clock = GameClock::fromStorage($gameStor); +$gameNow = $clock->nowDateTime(); $db->insert('ng_diplomacy', [ 'src_nation_id'=>$srcNation['nation'], 'dest_nation_id'=>$destNation['nation'], @@ -147,7 +149,7 @@ $db->insert('ng_diplomacy', [ 'state'=>'proposed', 'text_brief'=>$textBrief, 'text_detail'=>$textDetail, - 'date'=>TimeUtil::now(), + 'date'=>TimeUtil::format($gameNow), 'src_signer'=>$me['no'], 'dest_signer'=>null, 'aux'=>Json::encode([ @@ -168,7 +170,7 @@ $newLetterNo = $db->insertId(); $src = new MessageTarget($me['no'], $me['name'], $srcNation['nation'], $srcNation['name'], $srcNation['color'], $me['icon']); $dest = new MessageTarget(0, '', $destNation['nation'], $destNation['name'], $destNation['color']); -$now = new \DateTime(); +$now = \DateTime::createFromImmutable($gameNow); $unlimited = new \DateTime('9999-12-31'); $josaYi = JosaUtil::pick($newLetterNo, '이'); @@ -195,4 +197,4 @@ Json::die([ 'result'=>true, 'reason'=>'success', 'row_id'=>$db->insertId() -]); \ No newline at end of file +]); diff --git a/hwe/j_get_select_npc_token.php b/hwe/j_get_select_npc_token.php index ccbcd081..a62c3868 100644 --- a/hwe/j_get_select_npc_token.php +++ b/hwe/j_get_select_npc_token.php @@ -79,7 +79,9 @@ if($token && !$refresh){ 'pick'=>Json::decode($token['pick_result']), 'pickMoreFrom'=>$clock->formatTick($pickMoreFrom), 'pickMoreSeconds'=>intdiv($pickMoreFrom - $now, $clock->ticksPerSecond()), - 'validUntil'=>$clock->formatTick(Util::toInt($token['valid_until'])) + 'validUntil'=>$clock->formatTick(Util::toInt($token['valid_until'])), + 'validForSeconds'=>max(0, intdiv(Util::toInt($token['valid_until']) - $now, $clock->ticksPerSecond())), + 'clockMode'=>$clock->getMode(), ]); } @@ -171,5 +173,7 @@ Json::die([ 'pick'=>$pickResult, 'pickMoreFrom'=>$clock->formatTick(($inserted===-1)?$pickMoreFrom:$now), 'pickMoreSeconds'=>($inserted===-1)?$pickMoreSecond:0, - 'validUntil'=>$clock->formatTick($validUntil) + 'validUntil'=>$clock->formatTick($validUntil), + 'validForSeconds'=>max(0, intdiv($validUntil - $now, $clock->ticksPerSecond())), + 'clockMode'=>$clock->getMode(), ]); diff --git a/hwe/j_get_select_pool.php b/hwe/j_get_select_pool.php index 9e3d9c0b..8121449d 100644 --- a/hwe/j_get_select_pool.php +++ b/hwe/j_get_select_pool.php @@ -86,7 +86,9 @@ if($tokens){ Json::die([ 'result'=>true, 'pick'=>$pick, - 'validUntil'=>$clock->formatTick(Util::toInt($valid_until)) + 'validUntil'=>$clock->formatTick(Util::toInt($valid_until)), + 'validForSeconds'=>max(0, intdiv(Util::toInt($valid_until) - $now, $clock->ticksPerSecond())), + 'clockMode'=>$clock->getMode(), ]); } @@ -106,5 +108,7 @@ sortTokens($pick);//좀 무식하지만.. Json::die([ 'result'=>true, 'pick'=>$pick, - 'validUntil'=>$valid_until === null ? null : $clock->formatTick(Util::toInt($valid_until)) -]); + 'validUntil'=>$valid_until === null ? null : $clock->formatTick(Util::toInt($valid_until)), + 'validForSeconds'=>$valid_until === null ? 0 : max(0, intdiv(Util::toInt($valid_until) - $now, $clock->ticksPerSecond())), + 'clockMode'=>$clock->getMode(), +]); diff --git a/hwe/j_myBossInfo.php b/hwe/j_myBossInfo.php index e4ff7e3c..9404571d 100644 --- a/hwe/j_myBossInfo.php +++ b/hwe/j_myBossInfo.php @@ -290,7 +290,7 @@ function do추방(General $general, int $myOfficerLevel): ?string $src, $src, $str, - new \DateTime(), + Message::gameNow(), new \DateTime('9999-12-31'), [] ); diff --git a/hwe/j_server_basic_info.php b/hwe/j_server_basic_info.php index d7868a56..27119356 100644 --- a/hwe/j_server_basic_info.php +++ b/hwe/j_server_basic_info.php @@ -76,6 +76,7 @@ $admin['maxUserCnt'] = $admin['maxgeneral']; $admin['npcMode'] = $admin['npcmode']; $admin['turnTerm'] = $admin['turnterm']; $admin['isUnited'] = $admin['isunited']; +$admin['isOpen'] = $clock->nowTick() >= Util::toInt($admin['opentime']); $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']); diff --git a/hwe/j_set_npc_control.php b/hwe/j_set_npc_control.php index bc8d0a96..f60bfa39 100644 --- a/hwe/j_set_npc_control.php +++ b/hwe/j_set_npc_control.php @@ -64,6 +64,7 @@ if ($permission < 3) { function applyNationPolicy($policy, $nationID, $generalName): ?string { $db = DB::db(); + $gameNow = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow(); $nationStor = KVStorage::getStorage($db, $nationID, 'nation_env'); $defaultPolicy = AutorunNationPolicy::$defaultPolicy; @@ -139,7 +140,7 @@ function applyNationPolicy($policy, $nationID, $generalName): ?string $nationPolicyRoot['values'] = $nationPolicy; $nationPolicyRoot['valueSetter'] = $generalName; - $nationPolicyRoot['valueSetTime'] = TimeUtil::now(); + $nationPolicyRoot['valueSetTime'] = $gameNow; $nationStor->npc_nation_policy = $nationPolicyRoot; return null; } @@ -147,6 +148,7 @@ function applyNationPolicy($policy, $nationID, $generalName): ?string function applyNationPriority($priority, $nationID, $generalName): ?string { $db = DB::db(); + $gameNow = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow(); $nationStor = KVStorage::getStorage($db, $nationID, 'nation_env'); $nationPolicyRoot = $nationStor->npc_nation_policy; @@ -158,7 +160,7 @@ function applyNationPriority($priority, $nationID, $generalName): ?string } $nationPolicyRoot['priority'] = $priority; $nationPolicyRoot['prioritySetter'] = $generalName; - $nationPolicyRoot['prioritySetTime'] = TimeUtil::now(); + $nationPolicyRoot['prioritySetTime'] = $gameNow; $nationStor->npc_nation_policy = $nationPolicyRoot; return null; } @@ -166,6 +168,7 @@ function applyNationPriority($priority, $nationID, $generalName): ?string function applyGeneralPriority($priority, $nationID, $generalName): ?string { $db = DB::db(); + $gameNow = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow(); $nationStor = KVStorage::getStorage($db, $nationID, 'nation_env'); $generalPolicyRoot = $nationStor->npc_general_policy; @@ -206,7 +209,7 @@ function applyGeneralPriority($priority, $nationID, $generalName): ?string $generalPolicyRoot['priority'] = $priority; $generalPolicyRoot['prioritySetter'] = $generalName; - $generalPolicyRoot['prioritySetTime'] = TimeUtil::now(); + $generalPolicyRoot['prioritySetTime'] = $gameNow; $nationStor->npc_general_policy = $generalPolicyRoot; return null; } diff --git a/hwe/sammo/API/Command/GetReservedCommand.php b/hwe/sammo/API/Command/GetReservedCommand.php index 195376ed..0a43ddfb 100644 --- a/hwe/sammo/API/Command/GetReservedCommand.php +++ b/hwe/sammo/API/Command/GetReservedCommand.php @@ -10,7 +10,6 @@ use sammo\GameConst; use sammo\GameClock; use sammo\Json; use sammo\KVStorage; -use sammo\TimeUtil; use sammo\Util; use function sammo\cutTurn; @@ -33,6 +32,7 @@ class GetReservedCommand extends \sammo\BaseAPI $commandList = []; $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); $generalID = $session->generalID; $invalidTurnList = 0; @@ -85,11 +85,12 @@ class GetReservedCommand extends \sammo\BaseAPI return [ 'result' => true, 'turnTimeTick' => Util::toInt($turnTime), - 'turnTime' => GameClock::fromStorage($gameStor)->formatTick(Util::toInt($turnTime)), + 'turnTime' => $clock->formatTick(Util::toInt($turnTime)), 'turnTerm' => $turnTerm, 'year' => $year, 'month' => $month, - 'date' => GameClock::fromStorage($gameStor)->formatTick(GameClock::fromStorage($gameStor)->nowTick(), true), + 'date' => $clock->formatTick($clock->nowTick(), true), + 'clockMode' => $clock->getMode(), 'turn' => $commandList, 'autorun_limit' => $generalAux['autorun_limit'] ?? null, ]; diff --git a/hwe/sammo/API/General/GetFrontInfo.php b/hwe/sammo/API/General/GetFrontInfo.php index 8fc44ef7..221a20fe 100644 --- a/hwe/sammo/API/General/GetFrontInfo.php +++ b/hwe/sammo/API/General/GetFrontInfo.php @@ -21,7 +21,6 @@ use sammo\LastTurn; use sammo\Validator; use sammo\Session; -use sammo\TimeUtil; use sammo\Util; use function sammo\buildNationCommandClass; @@ -185,8 +184,9 @@ class GetFrontInfo extends \sammo\BaseAPI $lastVote = null; if ($lastVoteID) { $voteStor = KVStorage::getStorage($db, 'vote'); - $lastVote = VoteInfo::fromArray($voteStor->getValue("vote_{$lastVoteID}")); - if ($lastVote->endDate && $lastVote->endDate < TimeUtil::now()) { + $rawLastVote = VoteInfo::normalizeGameStorage($voteStor->getValue("vote_{$lastVoteID}"), $clock); + $lastVote = VoteInfo::fromGameStorage($rawLastVote, $clock); + if ($rawLastVote['endTick'] !== null && $rawLastVote['endTick'] < $clock->nowTick()) { $lastVote = null; } } diff --git a/hwe/sammo/API/InheritAction/BuyRandomUnique.php b/hwe/sammo/API/InheritAction/BuyRandomUnique.php index 6e907b2e..05055a0b 100644 --- a/hwe/sammo/API/InheritAction/BuyRandomUnique.php +++ b/hwe/sammo/API/InheritAction/BuyRandomUnique.php @@ -10,7 +10,6 @@ use sammo\Enums\RankColumn; use sammo\GameConst; use sammo\General; use sammo\KVStorage; -use sammo\TimeUtil; use sammo\UserLogger; class BuyRandomUnique extends \sammo\BaseAPI @@ -56,7 +55,7 @@ class BuyRandomUnique extends \sammo\BaseAPI $userLogger->push("{$reqAmount} 포인트로 랜덤 유니크 구입", "inheritPoint"); $userLogger->flush(); - $general->setAuxVar('inheritRandomUnique', TimeUtil::now()); + $general->setAuxVar('inheritRandomUnique', true); $inheritStor->setValue('previous', [$previousPoint - $reqAmount, null]); $general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, $reqAmount); $general->applyDB($db); diff --git a/hwe/sammo/API/InheritAction/CheckOwner.php b/hwe/sammo/API/InheritAction/CheckOwner.php index 00efbdbc..6e0f742d 100644 --- a/hwe/sammo/API/InheritAction/CheckOwner.php +++ b/hwe/sammo/API/InheritAction/CheckOwner.php @@ -116,7 +116,7 @@ class CheckOwner extends \sammo\BaseAPI $src, $dest, "{$destGeneralName}의 소유자는 {$destGeneralOwnerName} 입니다.", - new \DateTime(), + Message::gameNow(), new \DateTime('9999-12-31'), [] ); @@ -142,7 +142,7 @@ class CheckOwner extends \sammo\BaseAPI $src, $dest, "소유자명이 누군가에 의해 확인되었습니다.", - new \DateTime(), + Message::gameNow(), new \DateTime('9999-12-31'), [] ); diff --git a/hwe/sammo/API/Message/SendMessage.php b/hwe/sammo/API/Message/SendMessage.php index bc44c92d..ed697f93 100644 --- a/hwe/sammo/API/Message/SendMessage.php +++ b/hwe/sammo/API/Message/SendMessage.php @@ -44,7 +44,7 @@ class SendMessage extends \sammo\BaseAPI private function genPublicMessage(MessageTarget $src, string $text): Message { - $now = new \DateTime(); + $now = Message::gameNow(); $unlimited = new \DateTime('9999-12-31'); $msg = new Message( @@ -62,7 +62,7 @@ class SendMessage extends \sammo\BaseAPI private function genNationalMessage(MessageTarget $src, string $text): Message { - $now = new \DateTime(); + $now = Message::gameNow(); $unlimited = new \DateTime('9999-12-31'); $dest = new MessageTarget(0, '', $src->nationID, $src->nationName, $src->color); @@ -82,7 +82,7 @@ class SendMessage extends \sammo\BaseAPI private function genDiplomacyMessage(MessageTarget $src, int $destNationID, string $text): Message|string { - $now = new \DateTime(); + $now = Message::gameNow(); $unlimited = new \DateTime('9999-12-31'); $destNation = getNationStaticInfo($destNationID); @@ -107,7 +107,7 @@ class SendMessage extends \sammo\BaseAPI private function genPrivateMessage(MessageTarget $src, int $destGeneralID, int $permission, string $text): Message|string { - $now = new \DateTime(); + $now = Message::gameNow(); $unlimited = new \DateTime('9999-12-31'); $db = DB::db(); diff --git a/hwe/sammo/API/Nation/SetNotice.php b/hwe/sammo/API/Nation/SetNotice.php index 8c2fa1aa..b0dac3ea 100644 --- a/hwe/sammo/API/Nation/SetNotice.php +++ b/hwe/sammo/API/Nation/SetNotice.php @@ -6,8 +6,8 @@ use sammo\Session; use DateTimeInterface; use sammo\DB; use sammo\Enums\APIRecoveryType; +use sammo\GameClock; use sammo\KVStorage; -use sammo\TimeUtil; use sammo\Validator; use sammo\WebUtil; @@ -51,8 +51,9 @@ class SetNotice extends \sammo\BaseAPI $nationID = $me['nation']; $nationStor = KVStorage::getStorage($db, $nationID, 'nation_env'); + $gameNow = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow(); $nationStor->nationNotice = [ - 'date'=>TimeUtil::now(), + 'date'=>$gameNow, 'msg'=>WebUtil::htmlPurify($msg), 'author'=>$me['name'], 'authorID'=>$me['no'], diff --git a/hwe/sammo/API/NationCommand/GetReservedCommand.php b/hwe/sammo/API/NationCommand/GetReservedCommand.php index 78af94eb..08e25eaf 100644 --- a/hwe/sammo/API/NationCommand/GetReservedCommand.php +++ b/hwe/sammo/API/NationCommand/GetReservedCommand.php @@ -12,7 +12,6 @@ use sammo\GameClock; use sammo\General; use sammo\Json; use sammo\KVStorage; -use sammo\TimeUtil; use sammo\Util; use function sammo\checkLimit; @@ -40,6 +39,7 @@ class GetReservedCommand extends \sammo\BaseAPI increaseRefresh("사령부", 1); $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); $userID = $session->userID; $me = $db->queryFirstRow( @@ -158,7 +158,8 @@ class GetReservedCommand extends \sammo\BaseAPI 'year' => $year, 'month' => $month, 'turnTerm' => $turnTerm, - 'date' => TimeUtil::now(true), + 'date' => $clock->formatTick($clock->nowTick(), true), + 'clockMode' => $clock->getMode(), 'chiefList' => $nationChiefList, 'troopList' => $troopList, 'isChief' => ($me['officer_level'] > 4), diff --git a/hwe/sammo/API/Vote/AddComment.php b/hwe/sammo/API/Vote/AddComment.php index 40b1899f..1bed91a0 100644 --- a/hwe/sammo/API/Vote/AddComment.php +++ b/hwe/sammo/API/Vote/AddComment.php @@ -10,8 +10,9 @@ use sammo\Enums\GeneralLiteQueryMode; use sammo\Enums\GeneralQueryMode; use sammo\General; use sammo\GeneralLite; +use sammo\GameClock; +use sammo\KVStorage; use sammo\Session; -use sammo\TimeUtil; use sammo\Validator; class AddComment extends \sammo\BaseAPI @@ -47,7 +48,8 @@ class AddComment extends \sammo\BaseAPI $generalName = $general->getName(); $nationID = $general->getNationID(); $nationName = $general->getStaticNation()['name']; - $date = TimeUtil::now(); + $db = DB::db(); + $date = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow(); $comment = new VoteComment( @@ -61,7 +63,6 @@ class AddComment extends \sammo\BaseAPI date: $date ); - $db = DB::db(); $db->insert('vote_comment', $comment->toArray()); return null; diff --git a/hwe/sammo/API/Vote/GetVoteDetail.php b/hwe/sammo/API/Vote/GetVoteDetail.php index 26496aef..b5636a30 100644 --- a/hwe/sammo/API/Vote/GetVoteDetail.php +++ b/hwe/sammo/API/Vote/GetVoteDetail.php @@ -8,6 +8,7 @@ use sammo\DB; use sammo\DTO\VoteComment; use sammo\DTO\VoteInfo; use sammo\Enums\APIRecoveryType; +use sammo\GameClock; use sammo\Json; use sammo\KVStorage; use sammo\Validator; @@ -35,13 +36,16 @@ class GetVoteDetail extends \sammo\BaseAPI { $voteID = $this->args['voteID']; $db = DB::db(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); $voteStor = KVStorage::getStorage($db, 'vote'); $rawVote = $voteStor->getValue("vote_{$voteID}"); if (!$rawVote) { return '설문조사가 없습니다.'; } - $voteInfo = VoteInfo::fromArray($rawVote); + $rawVote = VoteInfo::normalizeGameStorage($rawVote, $clock); + $voteInfo = VoteInfo::fromGameStorage($rawVote, $clock); + $isOpen = $rawVote['endTick'] === null || $rawVote['endTick'] >= $clock->nowTick(); $votes = array_map(fn ($arr) => [Json::decode($arr[0]), $arr[1]], $db->queryAllLists( @@ -70,6 +74,7 @@ class GetVoteDetail extends \sammo\BaseAPI 'comments' => $comments, 'myVote' => $myVote, 'userCnt' => $userCnt, + 'isOpen' => $isOpen, ]; } } diff --git a/hwe/sammo/API/Vote/GetVoteList.php b/hwe/sammo/API/Vote/GetVoteList.php index 810e151e..664a4988 100644 --- a/hwe/sammo/API/Vote/GetVoteList.php +++ b/hwe/sammo/API/Vote/GetVoteList.php @@ -6,6 +6,7 @@ use DateTimeInterface; use sammo\DB; use sammo\DTO\VoteInfo; use sammo\Enums\APIRecoveryType; +use sammo\GameClock; use sammo\KVStorage; use sammo\Session; @@ -25,16 +26,17 @@ class GetVoteList 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')); $voteStor = KVStorage::getStorage($db, 'vote'); $votes = []; foreach($voteStor->getAll() as $voteKey => $rawVote){ - if(!str_starts_with($voteKey, 'vote_')){ + if(preg_match('/^vote_(\d+)$/D', $voteKey, $matches) !== 1){ continue; } - $voteID = (int)substr($voteKey, 5); - $votes[$voteID] = VoteInfo::fromArray($rawVote); + $voteID = (int)$matches[1]; + $votes[$voteID] = VoteInfo::fromGameStorage($rawVote, $clock); } return [ diff --git a/hwe/sammo/API/Vote/NewVote.php b/hwe/sammo/API/Vote/NewVote.php index 507b759e..b12df17a 100644 --- a/hwe/sammo/API/Vote/NewVote.php +++ b/hwe/sammo/API/Vote/NewVote.php @@ -9,7 +9,7 @@ use sammo\Enums\APIRecoveryType; use sammo\KVStorage; use sammo\RootDB; use sammo\Session; -use sammo\TimeUtil; +use sammo\GameClock; use sammo\Util; use sammo\Validator; @@ -37,7 +37,7 @@ class NewVote extends \sammo\BaseAPI return null; } - function closeOldVote(int $voteID, KVStorage $voteStor) + function closeOldVote(int $voteID, KVStorage $voteStor, GameClock $clock) { $db = DB::db(); $voteStor = KVStorage::getStorage($db, 'vote'); @@ -45,13 +45,14 @@ class NewVote extends \sammo\BaseAPI if (!$rawLastVoteInfo) { return; } - $lastVoteInfo = VoteInfo::fromArray($rawLastVoteInfo); - if ($lastVoteInfo->endDate) { + $rawLastVoteInfo = VoteInfo::normalizeGameStorage($rawLastVoteInfo, $clock); + if ($rawLastVoteInfo['endTick'] !== null) { return; } - $lastVoteInfo->endDate = TimeUtil::now(); - $voteStor->setValue("vote_{$voteID}", $lastVoteInfo->toArray()); + $rawLastVoteInfo['endTick'] = $clock->nowTick(); + $rawLastVoteInfo['endDate'] = $clock->formatTick($rawLastVoteInfo['endTick']); + $voteStor->setValue("vote_{$voteID}", $rawLastVoteInfo); } function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType @@ -71,7 +72,11 @@ class NewVote extends \sammo\BaseAPI $multipleOptions = 0; } - $now = TimeUtil::now(); + $db = DB::db(); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $clock = GameClock::fromStorage($gameStor); + $nowTick = $clock->nowTick(); + $now = $clock->formatTick($nowTick); /** @var ?string */ $endDate = $this->args['endDate'] ?? null; /** @var string[] */ @@ -83,9 +88,9 @@ class NewVote extends \sammo\BaseAPI if($endDate !== null){ try{ - $oNow = new \DateTimeImmutable($now); $oEndDate = new \DateTimeImmutable($endDate); - if($oEndDate < $oNow){ + $endTick = $clock->dateTimeToTick($oEndDate); + if($endTick < $nowTick){ return '종료일이 이미 지났습니다.'; } } @@ -96,17 +101,13 @@ class NewVote extends \sammo\BaseAPI $userName = $session->userName; - $db = DB::db(); - $gameStor = KVStorage::getStorage($db, 'game_env'); - - $lastVote = $gameStor->getValue('lastVote') ?? 0; $voteID = $lastVote + 1; $voteStor = KVStorage::getStorage($db, 'vote'); if (!($this->args['keepOldVote'] ?? false)) { - $this->closeOldVote($lastVote, $voteStor); + $this->closeOldVote($lastVote, $voteStor, $clock); } $multipleOptions = Util::valueFit($multipleOptions, 0, count($options)); @@ -122,7 +123,10 @@ class NewVote extends \sammo\BaseAPI options: $options, ); - $voteStor->setValue("vote_{$voteID}", $voteInfo->toArray()); + $voteStor->setValue("vote_{$voteID}", $voteInfo->toArray() + [ + 'startTick' => $nowTick, + 'endTick' => $endDate === null ? null : $clock->dateTimeToTick(new \DateTimeImmutable($endDate)), + ]); $gameStor->setValue('lastVote', $voteID); $db->update('general', [ diff --git a/hwe/sammo/API/Vote/Vote.php b/hwe/sammo/API/Vote/Vote.php index fea230c3..885e90e0 100644 --- a/hwe/sammo/API/Vote/Vote.php +++ b/hwe/sammo/API/Vote/Vote.php @@ -8,6 +8,7 @@ use sammo\DTO\VoteInfo; use sammo\Enums\APIRecoveryType; use sammo\Enums\GeneralQueryMode; use sammo\General; +use sammo\GameClock; use sammo\Json; use sammo\KVStorage; use sammo\LiteHashDRBG; @@ -54,15 +55,17 @@ class Vote extends \sammo\BaseAPI return '선택한 항목이 없습니다.'; } $db = DB::db(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); $voteStor = KVStorage::getStorage($db, 'vote'); $rawVoteInfo = $voteStor->getValue("vote_{$voteID}"); if (!$rawVoteInfo) { return '설문조사가 없습니다.'; } - $voteInfo = VoteInfo::fromArray($rawVoteInfo); + $rawVoteInfo = VoteInfo::normalizeGameStorage($rawVoteInfo, $clock); + $voteInfo = VoteInfo::fromGameStorage($rawVoteInfo, $clock); - if ($voteInfo->endDate && $voteInfo->endDate < new \DateTimeImmutable()) { + if ($rawVoteInfo['endTick'] !== null && $rawVoteInfo['endTick'] < $clock->nowTick()) { return '설문조사가 종료되었습니다.'; } diff --git a/hwe/sammo/Command/Nation/che_몰수.php b/hwe/sammo/Command/Nation/che_몰수.php index f63a3cb8..20a429ed 100644 --- a/hwe/sammo/Command/Nation/che_몰수.php +++ b/hwe/sammo/Command/Nation/che_몰수.php @@ -193,7 +193,7 @@ class che_몰수 extends Command\NationCommand $src, $src, $text, - new \DateTime(), + Message::gameNow(), new \DateTime('9999-12-31'), [] ); diff --git a/hwe/sammo/Command/Nation/che_불가침제의.php b/hwe/sammo/Command/Nation/che_불가침제의.php index d33f76f5..a4057447 100644 --- a/hwe/sammo/Command/Nation/che_불가침제의.php +++ b/hwe/sammo/Command/Nation/che_불가침제의.php @@ -198,8 +198,9 @@ class che_불가침제의 extends Command\NationCommand $destNation['color'] ); - $now = new \DateTime($date); - $validUntil = new \DateTime($date); + $clock = \sammo\GameClock::fromStorage(\sammo\KVStorage::getStorage($db, 'game_env')); + $now = \DateTime::createFromImmutable($clock->tickToDateTime($general->getTurnTick())); + $validUntil = clone $now; $validMinutes = max(30, $env['turnterm'] * 3); $validUntil->add(new \DateInterval("PT{$validMinutes}M")); diff --git a/hwe/sammo/Command/Nation/che_불가침파기제의.php b/hwe/sammo/Command/Nation/che_불가침파기제의.php index 1cf27c35..9c46b8af 100644 --- a/hwe/sammo/Command/Nation/che_불가침파기제의.php +++ b/hwe/sammo/Command/Nation/che_불가침파기제의.php @@ -147,8 +147,9 @@ class che_불가침파기제의 extends Command\NationCommand{ $destNation['color'] ); - $now = new \DateTime($date); - $validUntil = new \DateTime($date); + $clock = \sammo\GameClock::fromStorage(\sammo\KVStorage::getStorage($db, 'game_env')); + $now = \DateTime::createFromImmutable($clock->tickToDateTime($general->getTurnTick())); + $validUntil = clone $now; $validMinutes = max(30, $env['turnterm']*3); $validUntil->add(new \DateInterval("PT{$validMinutes}M")); @@ -217,4 +218,4 @@ class che_불가침파기제의 extends Command\NationCommand{ ], ]; } -} \ No newline at end of file +} diff --git a/hwe/sammo/Command/Nation/che_종전제의.php b/hwe/sammo/Command/Nation/che_종전제의.php index 236cb4c3..49a0a6bc 100644 --- a/hwe/sammo/Command/Nation/che_종전제의.php +++ b/hwe/sammo/Command/Nation/che_종전제의.php @@ -145,8 +145,9 @@ class che_종전제의 extends Command\NationCommand{ $destNation['color'] ); - $now = new \DateTime($date); - $validUntil = new \DateTime($date); + $clock = \sammo\GameClock::fromStorage(\sammo\KVStorage::getStorage($db, 'game_env')); + $now = \DateTime::createFromImmutable($clock->tickToDateTime($general->getTurnTick())); + $validUntil = clone $now; $validMinutes = max(30, $env['turnterm']*3); $validUntil->add(new \DateInterval("PT{$validMinutes}M")); @@ -203,4 +204,4 @@ class che_종전제의 extends Command\NationCommand{ ], ]; } -} \ No newline at end of file +} diff --git a/hwe/sammo/DTO/VoteInfo.php b/hwe/sammo/DTO/VoteInfo.php index e6f817ab..22985a69 100644 --- a/hwe/sammo/DTO/VoteInfo.php +++ b/hwe/sammo/DTO/VoteInfo.php @@ -2,8 +2,41 @@ namespace sammo\DTO; +use sammo\GameClock; +use sammo\Util; + class VoteInfo extends \LDTO\DTO { + /** + * 기존 문자열만 가진 vote도 읽되, 저장 경계에서는 반드시 tick을 함께 둡니다. + * + * @return array + */ + public static function normalizeGameStorage(array $raw, GameClock $clock): array + { + if (!array_key_exists('startTick', $raw)) { + $raw['startTick'] = $clock->dateTimeToTick(new \DateTimeImmutable((string)$raw['startDate'])); + } + if (!array_key_exists('endTick', $raw)) { + $raw['endTick'] = ($raw['endDate'] ?? null) === null + ? null + : $clock->dateTimeToTick(new \DateTimeImmutable((string)$raw['endDate'])); + } + + $raw['startTick'] = Util::toInt($raw['startTick']); + $raw['endTick'] = $raw['endTick'] === null ? null : Util::toInt($raw['endTick']); + $raw['startDate'] = $clock->formatTick($raw['startTick']); + $raw['endDate'] = $raw['endTick'] === null ? null : $clock->formatTick($raw['endTick']); + return $raw; + } + + public static function fromGameStorage(array $raw, GameClock $clock): self + { + $raw = self::normalizeGameStorage($raw, $clock); + unset($raw['startTick'], $raw['endTick']); + return self::fromArray($raw); + } + public function __construct( public int $id, public string $title, diff --git a/hwe/sammo/DiplomaticMessage.php b/hwe/sammo/DiplomaticMessage.php index a439efa1..978747ba 100644 --- a/hwe/sammo/DiplomaticMessage.php +++ b/hwe/sammo/DiplomaticMessage.php @@ -215,7 +215,7 @@ class DiplomaticMessage extends Message{ $this->dest, $this->src, "【외교】{$year}년 {$month}월: {$this->src->nationName}{$josaYi} {$this->dest->nationName}에게 제안한 {$this->diplomacyDetail}", - new \DateTime(), + Message::gameNow(), new \DateTime('9999-12-31'), [ 'delete'=>$this->id, @@ -231,7 +231,7 @@ class DiplomaticMessage extends Message{ $this->dest, $this->src, "【외교】{$year}년 {$month}월: {$this->src->nationName}{$josaYi} {$this->dest->nationName}에게 제안한 {$this->diplomacyDetail}", - new \DateTime(), + Message::gameNow(), new \DateTime('9999-12-31'), [ 'delete'=>$this->id, diff --git a/hwe/sammo/Event/Action/OpenNationBetting.php b/hwe/sammo/Event/Action/OpenNationBetting.php index faec3be3..98440600 100644 --- a/hwe/sammo/Event/Action/OpenNationBetting.php +++ b/hwe/sammo/Event/Action/OpenNationBetting.php @@ -123,7 +123,7 @@ class OpenNationBetting extends \sammo\Event\Action } $logger->flush(); - $now = new DateTime(); + $now = Message::gameNow(); $text = "새로운 {$name} 내기가 열렸습니다. 천통국 베팅란을 확인해주세요."; $src = new MessageTarget(0, '', 0, 'System', '#000000'); diff --git a/hwe/sammo/GeneralAI.php b/hwe/sammo/GeneralAI.php index 9a54f03d..cd56768c 100644 --- a/hwe/sammo/GeneralAI.php +++ b/hwe/sammo/GeneralAI.php @@ -3727,7 +3727,7 @@ class GeneralAI $src, $src, $general->getVar('npcmsg'), - new \DateTime(), + Message::gameNow(), new \DateTime('9999-12-31'), [] ); diff --git a/hwe/sammo/Message.php b/hwe/sammo/Message.php index 169e4103..1a140d4f 100644 --- a/hwe/sammo/Message.php +++ b/hwe/sammo/Message.php @@ -26,6 +26,12 @@ class Message ) { } + public static function gameNow(): \DateTime + { + $clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); + return \DateTime::createFromImmutable($clock->nowDateTime()); + } + public function setSentInfo(int $mailbox, int $messageID) : self { if(!Message::isValidMailBox($mailbox)){ @@ -78,6 +84,15 @@ class Message } public function toArray():array{ + $clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); + $messageTick = $clock->dateTimeToTick($this->date); + $deleteUntilTick = GameClock::addTicks($messageTick, $clock->ticksFromMinutes(5)); + $deleteRemainingTicks = max(0, $deleteUntilTick - $clock->nowTick()); + $deleteRemainingTicks = min($deleteRemainingTicks, $clock->ticksFromSeconds(2_147_483)); + $deleteRemainingMilliseconds = intdiv( + $deleteRemainingTicks * 1000, + $clock->ticksPerSecond(), + ); if($this->msgType === MessageType::public){ $src = $this->src->toArray(); $dest = null; @@ -98,7 +113,9 @@ class Message 'dest'=>$dest, 'text'=>$this->msg, 'option'=>$this->msgOption, - 'time'=>$this->date->format('Y-m-d H:i:s') + 'time'=>$this->date->format('Y-m-d H:i:s'), + 'deleteRemainingMilliseconds'=>$deleteRemainingMilliseconds, + 'clockMode'=>$clock->getMode(), ]; } @@ -446,7 +463,7 @@ class Message $src, $dest, $msg, - new \DateTime(), + self::gameNow(), new \DateTime('9999-12-31'), [] ); @@ -480,6 +497,8 @@ class Message } public function invalidate(?array $newMsgOption=null, bool $hideMsg=true){ + $clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env')); + $validUntilTick = $clock->dateTimeToTick($this->validUntil); if($newMsgOption !== null){ $this->msgOption = $newMsgOption; } @@ -487,7 +506,8 @@ class Message $this->msgOption['invalid'] = true; if($hideMsg){ - $this->validUntil = new \DateTime('2000-12-31'); + $validUntilTick = GameClock::addTicks($clock->nowTick(), -1); + $this->validUntil = \DateTime::createFromImmutable($clock->tickToDateTime($validUntilTick)); } else{ if(key_exists('receiverMessageID', $this->msgOption)){ @@ -505,8 +525,7 @@ class Message 'text' => $this->msg, 'option' => $this->msgOption ]), - 'valid_until'=>GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')) - ->dateTimeToTick($this->validUntil), + 'valid_until'=>$validUntilTick, ], 'id=%i', $this->id); } diff --git a/hwe/sammo/RaiseInvaderMessage.php b/hwe/sammo/RaiseInvaderMessage.php index e70fee8e..ab840713 100644 --- a/hwe/sammo/RaiseInvaderMessage.php +++ b/hwe/sammo/RaiseInvaderMessage.php @@ -142,7 +142,7 @@ class RaiseInvaderMessage extends Message $srcTarget = MessageTarget::buildSystemTarget(); $destTarget = MessageTarget::buildQuick($destGeneralID); if ($date === null) { - $date = new \DateTime(); + $date = Message::gameNow(); } /** diff --git a/hwe/sammo/ScoutMessage.php b/hwe/sammo/ScoutMessage.php index e70aff45..202b4a1a 100644 --- a/hwe/sammo/ScoutMessage.php +++ b/hwe/sammo/ScoutMessage.php @@ -108,7 +108,7 @@ class ScoutMessage extends Message $this->src, $this->dest, "{$this->src->nationName}{$josaRo} 등용 제의 수락", - new \DateTime(), + Message::gameNow(), new \DateTime('9999-12-31'), [ 'delete' => $this->id @@ -156,7 +156,7 @@ class ScoutMessage extends Message $this->src, $this->dest, "{$this->src->nationName}{$josaRo} 등용 제의 거부", - new \DateTime(), + Message::gameNow(), new \DateTime('9999-12-31'), [ 'delete' => $this->id @@ -205,9 +205,9 @@ class ScoutMessage extends Message $db = DB::db(); $srcGeneral = $db->queryFirstRow('SELECT `name`, nation FROM general WHERE `no`=%i', $srcGeneralID); $destGeneral = $db->queryFirstRow('SELECT `name`, nation, `officer_level` FROM general WHERE `no`=%i', $destGeneralID); - if ($date === null) { - $date = new \DateTime(); - } + if ($date === null) { + $date = Message::gameNow(); + } if ($destGeneral['officer_level'] == 12) { if ($reason !== null) { diff --git a/hwe/sammo/TurnExecutionHelper.php b/hwe/sammo/TurnExecutionHelper.php index 761af2ef..3dc67ce4 100644 --- a/hwe/sammo/TurnExecutionHelper.php +++ b/hwe/sammo/TurnExecutionHelper.php @@ -6,15 +6,20 @@ use sammo\Enums\EventTarget; use sammo\Enums\InheritanceKey; use \Symfony\Component\Lock; -class TurnExecutionHelper -{ +class TurnExecutionHelper +{ /** @var General*/ protected $generalObj; - public function __construct(General $general) + public function __construct(General $general) { $this->generalObj = $general; - } + } + + public static function monotonicCompletionTick(int $completedTick, int $candidateTick): int + { + return max($completedTick, $candidateTick); + } public function __destruct() { @@ -450,9 +455,12 @@ class TurnExecutionHelper updateTraffic(); if ($executionOver) { - if ($currentTurn !== null) { - $executed = true; - $gameStor->turntime = $currentTurn; + if ($currentTurn !== null) { + $executed = true; + $gameStor->turntime = self::monotonicCompletionTick( + Util::toInt($gameStor->turntime), + $currentTurn, + ); } unlock(); return $gameStor->turntime; @@ -497,10 +505,16 @@ class TurnExecutionHelper $gameStor->month ); - if ($currentTurn !== null) { - $executed = true; - $gameStor->turntime = $currentTurn; - } + if ($currentTurn !== null) { + $executed = true; + // A general's sub-tick can be just before the monthly boundary that + // was completed above. Never move the global completion cursor back + // behind an already-applied monthly event. + $gameStor->turntime = self::monotonicCompletionTick( + Util::toInt($gameStor->turntime), + $currentTurn, + ); + } //토너먼트 처리 processTournament(); diff --git a/hwe/sammo/UserLogger.php b/hwe/sammo/UserLogger.php index 0ce91599..8ee22cd2 100644 --- a/hwe/sammo/UserLogger.php +++ b/hwe/sammo/UserLogger.php @@ -55,7 +55,8 @@ class UserLogger } $db = DB::db(); - $date = TimeUtil::now(); + $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')); + $date = $clock->formatTick($clock->nowTick()); $serverID = UniqueConst::$serverID; $request = array_map(function ($textAndType) use ($date, $serverID) { [$text, $type] = $textAndType; diff --git a/hwe/sql/schema.sql b/hwe/sql/schema.sql index db095f46..6eeefdfb 100644 --- a/hwe/sql/schema.sql +++ b/hwe/sql/schema.sql @@ -297,7 +297,7 @@ CREATE TABLE IF NOT EXISTS `ng_old_nations` ( `server_id` CHAR(20) NOT NULL DEFAULT '0', `nation` INT(11) NOT NULL DEFAULT '0', `data` LONGTEXT NOT NULL DEFAULT '{}' COLLATE 'utf8mb4_bin', - `date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `date` DATETIME NOT NULL, PRIMARY KEY (`id`), INDEX `server_id` (`server_id`, `nation`), CONSTRAINT `json` CHECK (json_valid(`data`)) @@ -395,7 +395,7 @@ CREATE TABLE `ng_diplomacy` ( `state` ENUM('proposed', 'activated', 'cancelled', 'replaced') NOT NULL DEFAULT 'proposed', `text_brief` TEXT NOT NULL, `text_detail` TEXT NOT NULL, - `date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `date` DATETIME NOT NULL, `src_signer` INT(11) NOT NULL, `dest_signer` INT(11) NULL DEFAULT NULL, `aux` TEXT NULL DEFAULT NULL COLLATE 'utf8mb4_bin', diff --git a/hwe/ts/PageChiefCenter.vue b/hwe/ts/PageChiefCenter.vue index 2ec3ec78..d9722ff6 100644 --- a/hwe/ts/PageChiefCenter.vue +++ b/hwe/ts/PageChiefCenter.vue @@ -42,6 +42,7 @@ :maxTurn="maxChiefTurn" :maxPushTurn="Math.floor(maxChiefTurn / 2)" :date="date" + :clockMode="clockMode" :officer="officer" @raiseReload="reloadTable()" /> @@ -139,6 +140,7 @@ const tableObj = reactive, "result">>({ month: undefined, turnTerm: undefined, date: undefined, + clockMode: undefined, troopList: undefined, chiefList: undefined, isChief: undefined, @@ -149,7 +151,7 @@ const tableObj = reactive, "result">>({ unitSet: undefined, }); -const { year, month, turnTerm, date, chiefList, troopList, officerLevel, commandList } = toRefs(tableObj); +const { year, month, turnTerm, date, clockMode, chiefList, troopList, officerLevel, commandList } = toRefs(tableObj); let postFilterNationCommand = function (turnObj: TurnObj): TurnObj { return turnObj; diff --git a/hwe/ts/PageVote.vue b/hwe/ts/PageVote.vue index cf2fc070..fb17ccde 100644 --- a/hwe/ts/PageVote.vue +++ b/hwe/ts/PageVote.vue @@ -195,7 +195,6 @@ import { onMounted, reactive, ref, watch, computed } from "vue"; import type { VoteInfo, VoteDetailResult } from "@/defs/API/Vote"; import { SammoAPI } from "@/SammoAPI"; import { isString, range, sum } from "lodash-es"; -import { formatTime } from "@/util/formatTime"; import { isBrightColor } from "@/util/isBrightColor"; import { formatVoteColor } from "@/utilGame/formatVoteColor"; @@ -239,14 +238,7 @@ const canVote = computed(() => { if (currentVote.value.myVote) { return false; } - const endDate = currentVote.value.voteInfo.endDate; - if (endDate) { - const now = formatTime(new Date()); - if (now > endDate) { - return false; - } - } - return true; + return currentVote.value.isOpen; }); const currentVoteID = ref(); diff --git a/hwe/ts/PartialReservedCommand.vue b/hwe/ts/PartialReservedCommand.vue index 9ae40697..79b5c759 100644 --- a/hwe/ts/PartialReservedCommand.vue +++ b/hwe/ts/PartialReservedCommand.vue @@ -13,7 +13,7 @@ class="col alert alert-primary m-0 p-0" style="text-align: center; display: flex; justify-content: center; align-items: center" > - +
@@ -467,6 +467,7 @@ async function pushGeneralCommand(amount: number) { } const serverNow = ref(new Date()); +const clockRunning = ref(true); function pushGeneralCommandSingle(e: Event) { //NOTE: split 구현에 버그가 있어서, 수동으로 구분해야함 @@ -548,6 +549,7 @@ async function reloadCommandList() { } serverNow.value = parseTime(result.date); + clockRunning.value = result.clockMode === "realtime"; } async function reserveCommandDirect(args: [number[], TurnObj][], reload = true): Promise { diff --git a/hwe/ts/components/ChiefReservedCommand.vue b/hwe/ts/components/ChiefReservedCommand.vue index 94ac76fd..dcd9cf5d 100644 --- a/hwe/ts/components/ChiefReservedCommand.vue +++ b/hwe/ts/components/ChiefReservedCommand.vue @@ -27,7 +27,7 @@
- +
@@ -289,6 +289,10 @@ const props = defineProps({ maxTurn: VueTypes.integer.isRequired, maxPushTurn: VueTypes.integer.isRequired, date: VueTypes.string.isRequired, + clockMode: { + type: String as PropType<"realtime" | "manual">, + required: true, + }, year: VueTypes.integer.isRequired, month: VueTypes.integer.isRequired, turnTerm: VueTypes.integer.isRequired, diff --git a/hwe/ts/components/MessagePlate.vue b/hwe/ts/components/MessagePlate.vue index a9d53e67..de87f40f 100644 --- a/hwe/ts/components/MessagePlate.vue +++ b/hwe/ts/components/MessagePlate.vue @@ -126,8 +126,6 @@ diff --git a/hwe/ts/defs/API/Command.ts b/hwe/ts/defs/API/Command.ts index fe3f6e16..84d749b3 100644 --- a/hwe/ts/defs/API/Command.ts +++ b/hwe/ts/defs/API/Command.ts @@ -7,6 +7,7 @@ export type ReservedCommandResponse = { year: number; month: number; date: string; + clockMode: "realtime" | "manual"; turn: TurnObj[]; autorun_limit: null | number; }; diff --git a/hwe/ts/defs/API/Message.ts b/hwe/ts/defs/API/Message.ts index b583bc8f..7af3e920 100644 --- a/hwe/ts/defs/API/Message.ts +++ b/hwe/ts/defs/API/Message.ts @@ -34,6 +34,8 @@ export type MsgItem = { delete?: number; }; time: string; + deleteRemainingMilliseconds: number; + clockMode: "realtime" | "manual"; }; export type MsgPrintItem = MsgItem & { @@ -77,4 +79,4 @@ export type MailboxItem = { export type MabilboxListResponse = { result: true, nation: MailboxItem[] -} \ No newline at end of file +} diff --git a/hwe/ts/defs/API/NationCommand.ts b/hwe/ts/defs/API/NationCommand.ts index 2b00e55c..fbfe3004 100644 --- a/hwe/ts/defs/API/NationCommand.ts +++ b/hwe/ts/defs/API/NationCommand.ts @@ -7,6 +7,7 @@ export type ChiefResponse = { month: number; turnTerm: number; date: string; + clockMode: "realtime" | "manual"; chiefList: Record< number, { @@ -28,4 +29,4 @@ export type ChiefResponse = { }[]; mapName: string, unitSet: string, -}; \ No newline at end of file +}; diff --git a/hwe/ts/defs/API/Vote.ts b/hwe/ts/defs/API/Vote.ts index f0dddd5b..60b5cd56 100644 --- a/hwe/ts/defs/API/Vote.ts +++ b/hwe/ts/defs/API/Vote.ts @@ -30,4 +30,5 @@ export type VoteDetailResult = ValidResponse & { comments: VoteComment[], myVote: null|number[], userCnt: number, -} \ No newline at end of file + isOpen: boolean, +} diff --git a/hwe/ts/gateway/entrance.ts b/hwe/ts/gateway/entrance.ts index 510a884a..aa082eb8 100644 --- a/hwe/ts/gateway/entrance.ts +++ b/hwe/ts/gateway/entrance.ts @@ -5,7 +5,6 @@ import axios from 'axios'; import { initTooltip } from "@/legacy/initTooltip"; import { TemplateEngine } from '@util/TemplateEngine'; import type { InvalidResponse } from '@/defs'; -import { getDateTimeNow } from '@util/getDateTimeNow'; import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest'; import { loadPlugin as loadAdminPlugin } from '@/gateway/admin_server'; import '@/gateway/common'; @@ -108,6 +107,7 @@ type ReservedGameInfo = { type GameInfo = { isUnited: number, + isOpen: boolean, npcMode: '불가' | '가능' | '선택 생성', year: number, month: number, @@ -175,7 +175,6 @@ async function Entrance_UpdateServer() { async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) { const $serverList = $('#server_list'); - const now = getDateTimeNow(); const serverDetailInfoP: Record> = {}; @@ -239,7 +238,7 @@ async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) { } else if (game.isUnited == 2) { $serverHtml.find('.n_country').html('§천하통일§'); $serverHtml.find('.server_date').html(`${game.starttime}
~ ${game.turntime}`); - } else if (game.opentime <= now) { + } else if (game.isOpen) { $serverHtml.find('.n_country').html(`<${game.nationCnt}국 경쟁중>`); $serverHtml.find('.server_date').html(`${game.starttime} ~`); } else { @@ -247,7 +246,7 @@ async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) { $serverHtml.find('.server_date').html(`${game.starttime} ~`); } - if (game.opentime <= now) { + if (game.isOpen) { $serverHtml.append( TemplateEngine(serverTextInfo, game) ); @@ -294,4 +293,4 @@ async function Entrance_Logout() { return; } location.href = "../"; -} \ No newline at end of file +} diff --git a/hwe/ts/msg.ts b/hwe/ts/msg.ts index de7229fe..f3823e34 100644 --- a/hwe/ts/msg.ts +++ b/hwe/ts/msg.ts @@ -1,15 +1,11 @@ import $ from 'jquery'; import type { InvalidResponse } from '@/defs'; -import { getDateTimeNow } from '@util/getDateTimeNow'; import axios from 'axios'; import { convertFormData } from '@util/convertFormData'; import { isBrightColor } from "@util/isBrightColor"; import { unwrap } from '@util/unwrap'; import _, { isError, isString } from 'lodash-es'; -import { addMinutes } from 'date-fns'; -import { parseTime } from '@util/parseTime'; -import { formatTime } from '@util/formatTime'; import { TemplateEngine } from '@util/TemplateEngine'; import { isNotNull } from '@util/isNotNull'; import { unwrap_any } from '@util/unwrap_any'; @@ -32,8 +28,8 @@ const messageTemplate = `
- <%if(!this.option.action && src.id == myGeneralID && now <= last5min && invalidType == 'msg_valid' && !deletable){%> - + <%if(!this.option.action && src.id == myGeneralID && deleteRemainingMilliseconds > 0 && invalidType == 'msg_valid' && !deletable){%> + <%}%> <%if(msgType == 'private') {%> <%if(src.name == generalName){%> @@ -101,8 +97,7 @@ type MsgPrintItem = MsgItem & { nationType: 'local' | 'src' | 'dest'; myGeneralID: number; allowButton: boolean; - last5min: string; - now: string; + eraseUntil: number; invalidType: 'msg_invalid' | 'msg_valid'; deletable: boolean; src: MsgTarget & { colorType: 'bright' | 'dark' }, @@ -217,7 +212,7 @@ async function showOldMsg(msgType: MsgType): Promise { function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse { function checkErasable(obj: MsgResponse) { - const now = getDateTimeNow(); + const now = Date.now(); $('.btn-delete-msg').each(function () { const $btn = $(this); const eraseUntil = $btn.data('erase_until'); @@ -257,7 +252,6 @@ function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse { let needRefreshLastContact = (msgType == 'private'); - const now = getDateTimeNow(); //list의 맨 앞이 가장 최신 메시지임. const $msgs: JQuery[] = msgSource.map(function (msg) { @@ -311,7 +305,9 @@ function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse { allowButton = true; } - const last5min = formatTime(addMinutes(parseTime(msg.time), 5)); + const eraseUntil = msg.clockMode === "manual" + ? Number.MAX_SAFE_INTEGER + : Date.now() + msg.deleteRemainingMilliseconds; let invalidType: MsgPrintItem['invalidType']; if (msg.option && msg.option.invalid) { invalidType = 'msg_invalid'; @@ -336,9 +332,8 @@ function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse { myGeneralID: unwrap(myGeneralID), src, dest, - now, allowButton, - last5min, + eraseUntil, invalidType, deletable, defaultIcon, @@ -653,4 +648,4 @@ $(async function ($) { const msgType = $this.data('msg_type'); void showOldMsg(msgType); }) -}); \ No newline at end of file +}); diff --git a/hwe/ts/select_general_from_pool.ts b/hwe/ts/select_general_from_pool.ts index 03801c77..ba72eb84 100644 --- a/hwe/ts/select_general_from_pool.ts +++ b/hwe/ts/select_general_from_pool.ts @@ -42,8 +42,12 @@ type CardItem = { type GeneralPoolResponse = { result: true, pick: CardItem[], - validUntil: string, -} + validUntil: string, + validForSeconds: number, + clockMode: "realtime" | "manual", +} + +let logicalClockRunning = true; declare const characterInfo: Record; declare const hasGeneralID: number; @@ -185,7 +189,10 @@ async function buildGeneral(e: JQuery.Event) { location.href = './'; } -function updateOutdateTimer() { +function updateOutdateTimer() { + if (!logicalClockRunning) { + return; + } const $validUntilText = $('#valid_until_text'); const now = Date.now(); const validUntil = $validUntilText.data('until'); @@ -204,10 +211,11 @@ function updateOutdateTimer() { setTimeout(updateOutdateTimer, 1000); } -function printGenerals(value: GeneralPoolResponse) { +function printGenerals(value: GeneralPoolResponse) { + logicalClockRunning = value.clockMode === "realtime"; $('.card_holder').empty(); $('#valid_until').show(); - $('#valid_until_text').html(value.validUntil).data('until', (new Date(value.validUntil)).getTime()).css('color', 'white'); + $('#valid_until_text').html(value.validUntil).data('until', Date.now() + value.validForSeconds * 1000).css('color', 'white'); $('#outdate_token').hide(); const pick = value.pick.map(v => v);//XXX: 의도가 뭐였지? clone? diff --git a/hwe/ts/select_npc.ts b/hwe/ts/select_npc.ts index 354fbd32..3adf5920 100644 --- a/hwe/ts/select_npc.ts +++ b/hwe/ts/select_npc.ts @@ -68,9 +68,13 @@ type NPCToken = { result: true, pick: Record, pickMoreFrom: string, - pickMoreSeconds: number, - validUntil: string, -} + pickMoreSeconds: number, + validUntil: string, + validForSeconds: number, + clockMode: "realtime" | "manual", +} + +let logicalClockRunning = true; const templateGeneralCard = '
\ @@ -146,7 +150,10 @@ async function pickGeneral(this: HTMLElement, e: JQuery.Event) { location.href = './'; } -function updateOutdateTimer() { +function updateOutdateTimer() { + if (!logicalClockRunning) { + return; + } const $validUntilText = $('#valid_until_text'); const now = Date.now(); const validUntil = $validUntilText.data('until'); @@ -165,8 +172,15 @@ function updateOutdateTimer() { setTimeout(updateOutdateTimer, 1000); } -function updatePickMoreTimer() { - const $btn = $('#btn_pick_more'); +function updatePickMoreTimer() { + const $btn = $('#btn_pick_more'); + + if (!logicalClockRunning) { + const remain = Number($btn.data('remaining')); + $btn.prop('disabled', remain > 0); + $btn.html(remain > 0 ? '다른 장수 보기(논리 시계 대기)' : '다른 장수 보기'); + return; + } const now = Date.now(); const remain = ($btn.data('available') - now) / 1000; @@ -181,13 +195,14 @@ function updatePickMoreTimer() { setTimeout(updatePickMoreTimer, 250); } -function printGenerals(value: NPCToken) { +function printGenerals(value: NPCToken) { + logicalClockRunning = value.clockMode === "realtime"; $('.card_holder').empty(); $('#valid_until').show(); - $('#valid_until_text').html(value.validUntil).data('until', (new Date(value.validUntil)).getTime()).css('color', 'white'); + $('#valid_until_text').html(value.validUntil).data('until', Date.now() + value.validForSeconds * 1000).css('color', 'white'); $('#outdate_token').hide(); const time = Date.now() + value.pickMoreSeconds * 1000; - $('#btn_pick_more').data('available', time).prop('disabled', true); + $('#btn_pick_more').data('available', time).data('remaining', value.pickMoreSeconds).prop('disabled', true); const pick = $.map(value.pick, function (value) { return value; diff --git a/scripts/README-game-clock.md b/scripts/README-game-clock.md index 729f8177..df418966 100644 --- a/scripts/README-game-clock.md +++ b/scripts/README-game-clock.md @@ -35,7 +35,12 @@ anchor에 고정하므로 표시 시각이 튀지 않습니다. ```bash php scripts/verify-game-clock-engine.php --apply --engine-calls=2 +php scripts/verify-game-clock-engine.php --apply --until-unification --max-months=2400 ``` 이 검증기는 manual mode만 허용하고, 엔진 호출 전후 clock tick이 벽시계 때문에 변하지 않았는지와 마지막 처리 tick이 현재 tick을 넘지 않았는지 검사합니다. +`--until-unification`은 달력이나 DB 시각을 고쳐 쓰지 않고, 매월 manual clock만 +정확히 다음 turn tick으로 옮긴 뒤 실제 `TurnExecutionHelper`를 반복 호출합니다. +각 호출에서 시계 고정과 처리 tick 상한을 재검사하고 `isunited=2|3`이 되지 않으면 +성공으로 취급하지 않습니다. 반드시 격리 복제 DB에서 실행하세요. diff --git a/scripts/migrate-game-clock.php b/scripts/migrate-game-clock.php index fcca414d..3d3317f6 100644 --- a/scripts/migrate-game-clock.php +++ b/scripts/migrate-game-clock.php @@ -4,6 +4,7 @@ declare(strict_types=1); use sammo\DB; +use sammo\DTO\VoteInfo; use sammo\GameClock; use sammo\Json; use sammo\KVStorage; @@ -269,6 +270,30 @@ try { 'last천도Trial', ); } + foreach ($db->query( + 'SELECT `key`, value FROM storage WHERE namespace = %s AND `key` LIKE %s AND `key` NOT LIKE %s', + 'vote', + 'vote\_%', + '%\_wall\_backup', + ) as $row) { + $rawVote = Json::decode((string)$row['value']); + if (!is_array($rawVote)) { + throw new RuntimeException("{$row['key']} vote 저장값이 객체가 아닙니다."); + } + $db->insertUpdate('storage', [ + 'namespace' => 'vote', + 'key' => "{$row['key']}_wall_backup", + 'value' => Json::encode($rawVote), + ]); + $db->update('storage', [ + 'value' => Json::encode(VoteInfo::normalizeGameStorage($rawVote, $conversionClock)), + ], 'namespace = %s AND `key` = %s', 'vote', $row['key']); + } + + // Historical/display DATETIME columns remain dates, but inserts must always + // receive a GameClock-projected value rather than silently reading MariaDB time. + $db->query('ALTER TABLE ng_old_nations MODIFY `date` DATETIME NOT NULL'); + $db->query('ALTER TABLE ng_diplomacy MODIFY `date` DATETIME NOT NULL'); $db->query( 'ALTER TABLE general ' diff --git a/scripts/verify-game-clock-engine.php b/scripts/verify-game-clock-engine.php index 432b76a3..2d3eab9c 100644 --- a/scripts/verify-game-clock-engine.php +++ b/scripts/verify-game-clock-engine.php @@ -19,16 +19,25 @@ $_SERVER['REQUEST_URI'] ??= '/cli/verify-game-clock-engine'; require dirname(__DIR__) . '/hwe/lib.php'; require dirname(__DIR__) . '/hwe/func.php'; +while (ob_get_level() > 0) { + ob_end_flush(); +} +ob_implicit_flush(true); -$options = getopt('', ['apply', 'engine-calls:']); +$options = getopt('', ['apply', 'engine-calls:', 'until-unification', 'max-months:']); if (!isset($options['apply'])) { - fwrite(STDERR, "Usage: php scripts/verify-game-clock-engine.php --apply [--engine-calls=N]\n"); + fwrite(STDERR, "Usage: php scripts/verify-game-clock-engine.php --apply [--engine-calls=N] [--until-unification --max-months=N]\n"); exit(2); } -$engineCalls = filter_var($options['engine-calls'] ?? '1', FILTER_VALIDATE_INT); +$untilUnification = isset($options['until-unification']); +$engineCalls = filter_var($options['engine-calls'] ?? ($untilUnification ? '100' : '1'), FILTER_VALIDATE_INT); if ($engineCalls === false || $engineCalls < 1 || $engineCalls > 1000) { throw new InvalidArgumentException('--engine-calls는 1..1000이어야 합니다.'); } +$maxMonths = filter_var($options['max-months'] ?? '2400', FILTER_VALIDATE_INT); +if ($maxMonths === false || $maxMonths < 1 || $maxMonths > 10000) { + throw new InvalidArgumentException('--max-months는 1..10000이어야 합니다.'); +} $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); @@ -39,6 +48,112 @@ if ($clock->getMode() !== GameClock::MODE_MANUAL) { $fixedNowTick = $clock->nowTick(); $before = $gameStor->getValues(['year', 'month', 'turntime']); + +if ($untilUnification) { + $startedAt = GameClock::readWallTime(); + $startProjection = $clock->formatTick($fixedNowTick, true); + $advancedMonths = 0; + $totalEngineCalls = 0; + while ($advancedMonths < $maxMonths) { + $gameStor->resetCache(); + $state = $gameStor->getValues(['year', 'month', 'turntime', 'turnterm', 'isunited']); + if (in_array(Util::toInt($state['isunited']), [2, 3], true)) { + break; + } + + $beforeYearMonth = Util::joinYearMonth(Util::toInt($state['year']), Util::toInt($state['month'])); + $clock = GameClock::fromStorage($gameStor); + $nextMonthBoundary = $clock->addTurns( + \sammo\cutTurn(Util::toInt($state['turntime']), Util::toInt($state['turnterm'])), + 1, + ); + $nextMonthTick = GameClock::addTicks($nextMonthBoundary, 1); + + if (!\sammo\tryLock()) { + throw new RuntimeException('manual clock 전진을 위한 GAME lock을 획득하지 못했습니다.'); + } + try { + $clock->persistTick($gameStor, $nextMonthTick, GameClock::MODE_MANUAL); + } finally { + $gameStor->resetCache(); + \sammo\unlock(); + } + + $monthAdvanced = false; + for ($call = 0; $call < $engineCalls; $call++) { + $executed = false; + $locked = false; + TurnExecutionHelper::executeAllCommand($executed, $locked); + $totalEngineCalls++; + $gameStor->resetCache(); + $clock = GameClock::fromStorage($gameStor); + if ($clock->getMode() !== GameClock::MODE_MANUAL || $clock->nowTick() !== $nextMonthTick) { + throw new RuntimeException('실제 턴 엔진 실행 중 manual clock 상태가 벽시계에 의해 바뀌었습니다.'); + } + $afterCall = $gameStor->getValues(['year', 'month', 'turntime', 'isunited']); + if (Util::toInt($afterCall['turntime']) > $nextMonthTick) { + throw new RuntimeException('마지막 실행 tick이 현재 manual clock tick을 넘어갔습니다.'); + } + if (in_array(Util::toInt($afterCall['isunited']), [2, 3], true)) { + $monthAdvanced = true; + break; + } + // turntime is the authoritative completed schedule boundary. Some + // legacy monthly state is cached until the next storage read, so do + // not spin merely because year/month from that same call is stale. + if (Util::toInt($afterCall['turntime']) >= $nextMonthBoundary) { + $monthAdvanced = true; + break; + } + $afterYearMonth = Util::joinYearMonth(Util::toInt($afterCall['year']), Util::toInt($afterCall['month'])); + if ($afterYearMonth !== $beforeYearMonth) { + $monthAdvanced = true; + break; + } + if ($locked) { + throw new RuntimeException('통일 전 실제 턴 엔진이 GAME lock 또는 동결 상태에 머물렀습니다.'); + } + } + if (!$monthAdvanced) { + throw new RuntimeException("한 달을 {$engineCalls}회 엔진 호출 안에 완료하지 못했습니다."); + } + + $advancedMonths++; + if ($advancedMonths % 12 === 0) { + $gameStor->resetCache(); + printf( + "progress months=%d game=%d-%02d clock_tick=%d nations=%d engine_calls=%d\n", + $advancedMonths, + Util::toInt($gameStor->year), + Util::toInt($gameStor->month), + GameClock::fromStorage($gameStor)->nowTick(), + Util::toInt($db->queryFirstField('SELECT COUNT(*) FROM nation WHERE level > 0')), + $totalEngineCalls, + ); + } + } + + $gameStor->resetCache(); + $finalState = $gameStor->getValues(['year', 'month', 'turntime', 'isunited']); + if (!in_array(Util::toInt($finalState['isunited']), [2, 3], true)) { + throw new RuntimeException("{$maxMonths}개월 안에 천하통일에 도달하지 못했습니다."); + } + $finalClock = GameClock::fromStorage($gameStor); + printf( + "UNIFIED months=%d engine_calls=%d game=%d-%02d isunited=%d clock_tick=%d projected=%s start_projected=%s wall_elapsed=%.6f\n", + $advancedMonths, + $totalEngineCalls, + Util::toInt($finalState['year']), + Util::toInt($finalState['month']), + Util::toInt($finalState['isunited']), + $finalClock->nowTick(), + $finalClock->formatNow(true), + $startProjection, + (float)GameClock::readWallTime()->format('U.u') - (float)$startedAt->format('U.u'), + ); + exit(0); +} + $executedCount = 0; for ($call = 0; $call < $engineCalls; $call++) { $executed = false; diff --git a/src/sammo/GameClock.php b/src/sammo/GameClock.php index 0103ff67..cc874989 100644 --- a/src/sammo/GameClock.php +++ b/src/sammo/GameClock.php @@ -117,6 +117,11 @@ final class GameClock return self::addTicks($this->anchorTick, $this->ticksBetween($this->wallAnchor, $this->wallNow())); } + public function nowDateTime(): \DateTimeImmutable + { + return $this->tickToDateTime($this->nowTick()); + } + public function ticksFromSeconds(int|float $seconds): int { if (is_int($seconds)) { @@ -207,6 +212,11 @@ final class GameClock return TimeUtil::format($this->tickToDateTime($tick), $withFraction); } + public function formatNow(bool $withFraction = false): string + { + return $this->formatTick($this->nowTick(), $withFraction); + } + public static function baseTimeForProjection( \DateTimeInterface $projectedTime, int $tick, @@ -249,7 +259,7 @@ final class GameClock public function advance(KVStorage $gameStor, int $deltaTick): int { - $nextTick = $this->nowTick() + $deltaTick; + $nextTick = self::addTicks($this->nowTick(), $deltaTick); $this->persistTick($gameStor, $nextTick); return $nextTick; } diff --git a/src/sammo/Session.php b/src/sammo/Session.php index 86ed2e82..0f749173 100644 --- a/src/sammo/Session.php +++ b/src/sammo/Session.php @@ -239,13 +239,16 @@ class Session $loginDate = $this->get($serverID.static::GAME_KEY_DATE); $generalID = $this->get($serverID.static::GAME_KEY_GENERAL_ID); $generalName = $this->get($serverID.static::GAME_KEY_GENERAL_NAME); - $deadTime = $this->get($serverID.static::GAME_KEY_EXPECTED_DEADTIME); + $deadTick = $this->get($serverID.static::GAME_KEY_EXPECTED_DEADTIME); - $now = time(); + $wallNow = time(); + $db = DB::db(); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $gameNowTick = GameClock::fromStorage($gameStor)->nowTick(); if ( $globalLoginDate < $loginDate && - $generalID && $generalName && $loginDate && $deadTime - && $loginDate + 1800 > $now && $deadTime > $now + $generalID && $generalName && $loginDate && $deadTick + && $loginDate + 1800 > $wallNow && $deadTick > $gameNowTick ) { //로그인 정보는 30분간 유지한다. if ($result !== null) { @@ -254,13 +257,10 @@ class Session return $this; } - if ($generalID || $generalName || $loginDate || $deadTime) { + if ($generalID || $generalName || $loginDate || $deadTick) { $this->logoutGame(); } - $db = DB::db(); - $gameStor = KVStorage::getStorage($db, 'game_env'); - $general = $db->queryFirstRow( 'SELECT `no`, `name`, `killturn`, `turntime` from general where `owner` = %i', $userID @@ -272,16 +272,15 @@ class Session return $this; } - $turnterm = $gameStor->turnterm; $isUnited = $gameStor->isunited != 0; $generalID = $general['no']; $generalName = $general['name']; - $nextTurn = new \DateTime($general['turntime']); - $nextTurn = $nextTurn->getTimestamp(); - - $deadTime = $nextTurn + $general['killturn'] * $turnterm; - if ($deadTime < $now && !$isUnited) { + $deadTick = GameClock::addTicks( + Util::toInt($general['turntime']), + Util::toInt($general['killturn']) * GameClock::TICKS_PER_TURN, + ); + if ($deadTick < $gameNowTick && !$isUnited) { $locked = $db->queryFirstField('SELECT plock FROM plock WHERE `type` = "GAME" LIMIT 1'); if (!$locked) { if ($result !== null) { @@ -291,10 +290,10 @@ class Session } } - $this->set($serverID.static::GAME_KEY_DATE, $now); + $this->set($serverID.static::GAME_KEY_DATE, $wallNow); $this->set($serverID.static::GAME_KEY_GENERAL_ID, $generalID); $this->set($serverID.static::GAME_KEY_GENERAL_NAME, $generalName); - $this->set($serverID.static::GAME_KEY_EXPECTED_DEADTIME, $deadTime); + $this->set($serverID.static::GAME_KEY_EXPECTED_DEADTIME, $deadTick); return $this; } diff --git a/tests/GameClockBoundaryTest.php b/tests/GameClockBoundaryTest.php index 033cfb00..31b763c0 100644 --- a/tests/GameClockBoundaryTest.php +++ b/tests/GameClockBoundaryTest.php @@ -18,6 +18,7 @@ final class GameClockBoundaryTest extends TestCase '/\bNOW\s*\(/i', '/\bCURRENT_TIMESTAMP\b/i', '/\bCURDATE\s*\(/i', + '/\btime\s*\(/i', ] as $pattern) { self::assertDoesNotMatchRegularExpression($pattern, $source, $relativePath); } @@ -25,7 +26,7 @@ final class GameClockBoundaryTest extends TestCase public static function gameSchedulingFiles(): array { - return array_map(static fn (string $path): array => [$path], [ + $paths = [ 'hwe/sammo/TurnExecutionHelper.php', 'hwe/sammo/Auction.php', 'hwe/sammo/AuctionBasicResource.php', @@ -36,7 +37,43 @@ final class GameClockBoundaryTest extends TestCase 'hwe/sammo/AbsFromUserPool.php', 'hwe/sammo/GeneralPool/RandomNameGeneral.php', 'hwe/sammo/API/General/DieOnPrestart.php', - ]); + 'hwe/sammo/Message.php', + 'hwe/sammo/DiplomaticMessage.php', + 'hwe/sammo/ScoutMessage.php', + 'hwe/sammo/RaiseInvaderMessage.php', + 'hwe/sammo/GeneralAI.php', + 'hwe/sammo/API/Vote/NewVote.php', + 'hwe/sammo/API/Vote/Vote.php', + 'hwe/sammo/API/Vote/GetVoteList.php', + 'hwe/sammo/API/Vote/GetVoteDetail.php', + 'hwe/sammo/API/Vote/AddComment.php', + 'hwe/sammo/API/Nation/SetNotice.php', + 'hwe/j_get_select_npc_token.php', + 'hwe/j_get_select_pool.php', + 'hwe/j_set_npc_control.php', + 'hwe/j_board_article_add.php', + 'hwe/j_board_comment_add.php', + 'hwe/a_traffic.php', + 'hwe/j_server_basic_info.php', + 'hwe/j_diplomacy_send_letter.php', + 'hwe/j_diplomacy_respond_letter.php', + 'hwe/j_diplomacy_destroy_letter.php', + 'hwe/j_diplomacy_rollback_letter.php', + ]; + foreach (['hwe/sammo/Command', 'hwe/sammo/Event'] as $relativeDirectory) { + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator( + __DIR__ . '/../' . $relativeDirectory, + \FilesystemIterator::SKIP_DOTS, + )); + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $paths[] = $relativeDirectory . '/' . $iterator->getSubPathName(); + } + } + } + $paths = array_values(array_unique($paths)); + sort($paths); + return array_map(static fn (string $path): array => [$path], $paths); } public function testTickSchemaDoesNotUseDatabaseDefaultsForGameSchedules(): void @@ -55,5 +92,55 @@ final class GameClockBoundaryTest extends TestCase ] as $expected) { self::assertStringContainsString($expected, $schema); } + self::assertDoesNotMatchRegularExpression('/\b(?:CURRENT_TIMESTAMP|NOW\s*\()/i', $schema); } + + public function testMonthlyTrafficTimestampUsesLogicalClock(): void + { + $source = file_get_contents(__DIR__ . '/../hwe/func.php'); + self::assertIsString($source); + self::assertMatchesRegularExpression( + '/function updateTraffic\(\).*?GameClock::fromStorage\(\$gameStor\)->formatNow\(\).*?function CheckOverhead\(/s', + $source, + ); + self::assertDoesNotMatchRegularExpression( + '/function updateTraffic\(\).*?TimeUtil::now\(.*?function CheckOverhead\(/s', + $source, + ); + } + + public function testGameLoginDeathCheckUsesTicksWhileSessionTtlRemainsOperational(): void + { + $source = file_get_contents(__DIR__ . '/../src/sammo/Session.php'); + self::assertIsString($source); + self::assertMatchesRegularExpression( + '/function loginGame\(.*?GameClock::fromStorage\(\$gameStor\)->nowTick\(\).*?GameClock::TICKS_PER_TURN.*?function logoutGame\(/s', + $source, + ); + self::assertDoesNotMatchRegularExpression( + '/function loginGame\(.*?new\s+\\?DateTime(?:Immutable)?\([^)]*turntime.*?function logoutGame\(/s', + $source, + ); + } + + public function testBrowserDoesNotCompareProjectedGameDatesToItsWallClock(): void + { + $expectations = [ + 'hwe/ts/PageVote.vue' => ['currentVote.value.isOpen'], + 'hwe/ts/components/MessagePlate.vue' => ['msg.clockMode === "manual"'], + 'hwe/ts/gateway/entrance.ts' => ['game.isOpen'], + 'hwe/ts/select_npc.ts' => ['logicalClockRunning'], + 'hwe/ts/select_general_from_pool.ts' => ['logicalClockRunning'], + ]; + foreach ($expectations as $path => $needles) { + $source = file_get_contents(__DIR__ . '/../' . $path); + self::assertIsString($source); + foreach ($needles as $needle) { + self::assertStringContainsString($needle, $source, $path); + } + } + self::assertStringNotContainsString('formatTime(new Date())', file_get_contents(__DIR__ . '/../hwe/ts/PageVote.vue')); + self::assertStringNotContainsString('game.opentime <= now', file_get_contents(__DIR__ . '/../hwe/ts/gateway/entrance.ts')); + } + } diff --git a/tests/GameClockTest.php b/tests/GameClockTest.php index 74a08208..d62cfc30 100644 --- a/tests/GameClockTest.php +++ b/tests/GameClockTest.php @@ -5,6 +5,7 @@ namespace sammo; use PHPUnit\Framework\TestCase; require_once __DIR__ . '/../src/sammo/GameClock.php'; +require_once __DIR__ . '/../hwe/sammo/TurnExecutionHelper.php'; final class GameClockTest extends TestCase { @@ -58,6 +59,7 @@ final class GameClockTest extends TestCase ); self::assertSame(123_456, $clock->nowTick()); + self::assertSame($clock->formatTick(123_456), $clock->formatNow()); self::assertFalse($wallRead); } @@ -96,4 +98,10 @@ final class GameClockTest extends TestCase $this->expectException(\OverflowException::class); GameClock::addTicks(GameClock::MAX_SAFE_TICK, 1); } + + public function testGlobalCompletionTickIsMonotonicAcrossSubTickExecution(): void + { + self::assertSame(36_000_000, TurnExecutionHelper::monotonicCompletionTick(36_000_000, 35_500_000)); + self::assertSame(36_500_000, TurnExecutionHelper::monotonicCompletionTick(36_000_000, 36_500_000)); + } } diff --git a/tests/VoteGameClockTest.php b/tests/VoteGameClockTest.php new file mode 100644 index 00000000..a6831b9e --- /dev/null +++ b/tests/VoteGameClockTest.php @@ -0,0 +1,56 @@ + 7, + 'title' => '논리 시계 투표', + 'multipleOptions' => 1, + 'opener' => 'SYSTEM', + 'startDate' => '2035-01-01 01:00:00', + 'endDate' => '2035-01-01 03:00:00', + 'options' => ['찬성', '반대'], + ]; + + $stored = VoteInfo::normalizeGameStorage($raw, $clock); + + self::assertSame(GameClock::TICKS_PER_TURN, $stored['startTick']); + self::assertSame(GameClock::TICKS_PER_TURN * 3, $stored['endTick']); + self::assertSame('2035-01-01 01:00:00', $stored['startDate']); + self::assertSame('2035-01-01 03:00:00', $stored['endDate']); + } + + public function testStoredTicksRemainAuthoritativeWhenProjectionBaseChanges(): void + { + $oldBase = new \DateTimeImmutable('2035-01-01 00:00:00'); + $newBase = new \DateTimeImmutable('2040-05-01 12:00:00'); + $clock = new GameClock($newBase, 60, 0, GameClock::MODE_MANUAL, $newBase); + $stored = [ + 'id' => 8, + 'title' => 'tick 우선', + 'multipleOptions' => 1, + 'opener' => null, + 'startDate' => $oldBase->format('Y-m-d H:i:s'), + 'endDate' => $oldBase->modify('+1 hour')->format('Y-m-d H:i:s'), + 'startTick' => GameClock::TICKS_PER_TURN * 2, + 'endTick' => GameClock::TICKS_PER_TURN * 4, + 'options' => ['A'], + ]; + + $normalized = VoteInfo::normalizeGameStorage($stored, $clock); + self::assertSame('2040-05-01 14:00:00', $normalized['startDate']); + self::assertSame('2040-05-01 16:00:00', $normalized['endDate']); + } +}