fix: complete logical clock wall-time isolation

This commit is contained in:
2026-08-04 20:37:56 +09:00
committed by Hide_D
parent 5d9512ced7
commit fc6f84ce23
67 changed files with 689 additions and 231 deletions
+3 -2
View File
@@ -25,13 +25,15 @@ if (!$v->validate()) {
$msg = Util::getPost('msg'); $msg = Util::getPost('msg');
$btn = Util::getPost('btn'); $btn = Util::getPost('btn');
$log = Util::getPost('log'); $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); $maxgeneral = Util::getPost('maxgeneral', 'int', GameConst::$defaultMaxGeneral);
$maxnation = Util::getPost('maxnation', 'int', GameConst::$defaultMaxNation); $maxnation = Util::getPost('maxnation', 'int', GameConst::$defaultMaxNation);
$startyear = Util::getPost('startyear', 'int', GameConst::$defaultStartYear); $startyear = Util::getPost('startyear', 'int', GameConst::$defaultStartYear);
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
$clock = GameClock::fromStorage($gameStor);
$starttime ??= $clock->formatTick($clock->nowTick());
$admin = getAdmin(); $admin = getAdmin();
@@ -43,7 +45,6 @@ switch ($btn) {
pushGlobalHistoryLog(["<R>★</><S>{$log}</>"]); pushGlobalHistoryLog(["<R>★</><S>{$log}</>"]);
break; break;
case "변경1": case "변경1":
$clock = GameClock::fromStorage($gameStor);
$gameStor->clock_base_time = TimeUtil::format(GameClock::baseTimeForProjection( $gameStor->clock_base_time = TimeUtil::format(GameClock::baseTimeForProjection(
new \DateTimeImmutable($starttime), new \DateTimeImmutable($starttime),
Util::toInt($gameStor->starttime), Util::toInt($gameStor->starttime),
+8 -8
View File
@@ -113,7 +113,7 @@ switch ($btn) {
case "경험치1000": case "경험치1000":
$text = $btn . " 지급!"; $text = $btn . " 지급!";
foreach ($genlist as $generalID) { 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); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -124,7 +124,7 @@ switch ($btn) {
case "공헌치1000": case "공헌치1000":
$text = $btn . " 지급!"; $text = $btn . " 지급!";
foreach ($genlist as $generalID) { 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); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -135,7 +135,7 @@ switch ($btn) {
case "보숙10000": case "보숙10000":
$text = "보병숙련도+10000 지급!"; $text = "보병숙련도+10000 지급!";
foreach ($genlist as $generalID) { 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); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -145,7 +145,7 @@ switch ($btn) {
case "궁숙10000": case "궁숙10000":
$text = "궁병숙련도+10000 지급!"; $text = "궁병숙련도+10000 지급!";
foreach ($genlist as $generalID) { 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); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -156,7 +156,7 @@ switch ($btn) {
$src = MessageTarget::buildQuick($session->generalID); $src = MessageTarget::buildQuick($session->generalID);
$text = "기병숙련도+10000 지급!"; $text = "기병숙련도+10000 지급!";
foreach ($genlist as $generalID) { 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); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -167,7 +167,7 @@ switch ($btn) {
$src = MessageTarget::buildQuick($session->generalID); $src = MessageTarget::buildQuick($session->generalID);
$text = "귀병숙련도+10000 지급!"; $text = "귀병숙련도+10000 지급!";
foreach ($genlist as $generalID) { 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); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -178,7 +178,7 @@ switch ($btn) {
$src = MessageTarget::buildQuick($session->generalID); $src = MessageTarget::buildQuick($session->generalID);
$text = "차병숙련도+10000 지급!"; $text = "차병숙련도+10000 지급!";
foreach ($genlist as $generalID) { 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); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -198,7 +198,7 @@ switch ($btn) {
case "메세지 전달": case "메세지 전달":
$text = $msg ?? ''; $text = $msg ?? '';
foreach ($genlist as $generalID) { 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); $msg->send(true);
} }
break; break;
+1 -3
View File
@@ -100,8 +100,6 @@ $sel[$type] = "selected";
throw new \Exception("알 수 없는 외교 상태: {$dip['state']}"); throw new \Exception("알 수 없는 외교 상태: {$dip['state']}");
} }
$date = TimeUtil::now();
echo " echo "
<tr> <tr>
<td align=center style=color:" . newColor($nationColor[$me]) . ";background-color:{$nationColor[$me]};>$nationName[$me]</td> <td align=center style=color:" . newColor($nationColor[$me]) . ";background-color:{$nationColor[$me]};>$nationName[$me]</td>
@@ -122,4 +120,4 @@ $sel[$type] = "selected";
</table> </table>
</body> </body>
</html> </html>
+2 -1
View File
@@ -9,6 +9,7 @@ include "func.php";
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
$clock = GameClock::fromStorage($gameStor);
increaseRefresh("갱신정보", 1); increaseRefresh("갱신정보", 1);
@@ -23,7 +24,7 @@ $recentTraffic[] = [
'month' => $admin['month'], 'month' => $admin['month'],
'refresh' => $admin['refresh'], 'refresh' => $admin['refresh'],
'online' => $curonline, 'online' => $curonline,
'date' => TimeUtil::now() 'date' => $clock->formatNow()
]; ];
if ($admin['maxrefresh'] == 0) { if ($admin['maxrefresh'] == 0) {
+13 -9
View File
@@ -1082,12 +1082,12 @@ function updateTraffic()
if(count($recentTraffic) >= 5){ if(count($recentTraffic) >= 5){
array_shift($recentTraffic); array_shift($recentTraffic);
} }
$recentTraffic[] = [ $recentTraffic[] = [
'year'=>$admin['year'], 'year'=>$admin['year'],
'month' => $admin['month'], 'month' => $admin['month'],
'refresh' => $admin['refresh'], 'refresh' => $admin['refresh'],
'online' => $online, 'online' => $online,
'date' => TimeUtil::now(), 'date' => GameClock::fromStorage($gameStor)->formatNow(),
]; ];
$gameStor->recentTraffic = $recentTraffic; $gameStor->recentTraffic = $recentTraffic;
@@ -1376,12 +1376,14 @@ function CheckHall($no)
return; return;
} }
$unitedDate = TimeUtil::now(); $clock = GameClock::fromStorage($gameStor);
$unitedDate = $clock->formatTick($clock->nowTick());
$nation = $generalObj->getStaticNation(); $nation = $generalObj->getStaticNation();
$serverCnt = $db->queryFirstField('SELECT count(*) FROM ng_games'); $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'); $ownerName = $generalObj->getVar('owner_name');
if ($generalObj->getVar('owner')) { if ($generalObj->getVar('owner')) {
@@ -1786,11 +1788,13 @@ function deleteNation(General $lord, bool $applyDB): array
// 부대 삭제 // 부대 삭제
$db->delete('troop', 'nation=%i', $nationID); $db->delete('troop', 'nation=%i', $nationID);
// 국가 삭제 // 국가 삭제
$db->insert('ng_old_nations', [ $gameDate = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow();
'server_id' => UniqueConst::$serverID, $db->insert('ng_old_nations', [
'nation' => $nationID, 'server_id' => UniqueConst::$serverID,
'data' => Json::encode($nation) 'nation' => $nationID,
'date' => $gameDate,
'data' => Json::encode($nation)
]); ]);
$db->delete('nation', 'nation=%i', $nationID); $db->delete('nation', 'nation=%i', $nationID);
$db->delete('nation_turn', 'nation_id=%i', $nationID); $db->delete('nation_turn', 'nation_id=%i', $nationID);
+13 -10
View File
@@ -838,20 +838,23 @@ function checkEmperior()
$nation['aux'] += $nationStor->max_power ?? []; $nation['aux'] += $nationStor->max_power ?? [];
$nation['history'] = getNationHistoryLogAll($nation['nation']); $nation['history'] = getNationHistoryLogAll($nation['nation']);
storeOldGenerals(0, $admin['year'], $admin['month']); storeOldGenerals(0, $admin['year'], $admin['month']);
storeOldGenerals($nation['nation'], $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, $db->insert('ng_old_nations', [
'nation' => $nation['nation'], 'server_id' => UniqueConst::$serverID,
'data' => Json::encode($nation) 'nation' => $nation['nation'],
'date' => $gameDate,
'data' => Json::encode($nation)
]); ]);
$noNationGeneral = $db->queryFirstColumn('SELECT `no` FROM general WHERE nation=0'); $noNationGeneral = $db->queryFirstColumn('SELECT `no` FROM general WHERE nation=0');
$db->insert('ng_old_nations', [ $db->insert('ng_old_nations', [
'server_id' => UniqueConst::$serverID, 'server_id' => UniqueConst::$serverID,
'nation' => 0, 'nation' => 0,
'data' => Json::encode([ 'date' => $gameDate,
'data' => Json::encode([
'nation' => 0, 'nation' => 0,
'name' => '재야', 'name' => '재야',
'generals' => $noNationGeneral 'generals' => $noNationGeneral
+3 -2
View File
@@ -7,6 +7,7 @@ include "func.php";
Session::requireLogin()->loginGame()->setReadOnly(); Session::requireLogin()->loginGame()->setReadOnly();
$mapName = GameConst::$mapName; $mapName = GameConst::$mapName;
$frontClock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env'));
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
@@ -28,7 +29,7 @@ $mapName = GameConst::$mapName;
'maxTurn' => GameConst::$maxTurn, 'maxTurn' => GameConst::$maxTurn,
'maxPushTurn' => 12, 'maxPushTurn' => 12,
'serverNow' => TimeUtil::now(false), 'serverNow' => $frontClock->formatTick($frontClock->nowTick()),
] ]
], false) ?> ], false) ?>
<?= WebUtil::printJS('../d_shared/common_path.js') ?> <?= WebUtil::printJS('../d_shared/common_path.js') ?>
@@ -42,4 +43,4 @@ $mapName = GameConst::$mapName;
<div id="app"></div> <div id="app"></div>
</body> </body>
</html> </html>
+15 -12
View File
@@ -25,21 +25,24 @@ if(!$reserved){
]); ]);
} }
$reservedDate = new \DateTime($reserved['date']); $reservedDate = new \DateTimeImmutable($reserved['date']);
$now = new \DateTime(); $now = GameClock::readWallTime();
$status = 'not_yet'; $status = 'not_yet';
list($isUnited, $lastTurn) = $gameStor->getValuesAsArray(['isunited', 'turntime']); list($isUnited, $unitedWallAnchor) = $gameStor->getValuesAsArray(['isunited', 'autoreset_united_wall_anchor']);
if($isUnited === null || $lastTurn === null){ if($isUnited === null){
$isUnited = 2; $isUnited = 2;
$lastTurn = '2000-01-01'; }
} $lastTurn = null;
if($isUnited > 0){
if($lastTurn !== null){ if(!is_string($unitedWallAnchor) || $unitedWallAnchor === ''){
$lastTurn = new \DateTime($lastTurn); $unitedWallAnchor = TimeUtil::format($now, true);
} $gameStor->autoreset_united_wall_anchor = $unitedWallAnchor;
}
$lastTurn = new \DateTimeImmutable($unitedWallAnchor);
}
if($lastTurn === null){ if($lastTurn === null){
//이미 리셋된 상태임 //이미 리셋된 상태임
@@ -99,4 +102,4 @@ $result['affected']=1;
$prefix = DB::prefix(); $prefix = DB::prefix();
ServConfig::getServerList()[$prefix]->openServer(); ServConfig::getServerList()[$prefix]->openServer();
Json::die($result); Json::die($result);
+3 -2
View File
@@ -10,6 +10,7 @@ $session = Session::requireGameLogin()->setReadOnly();
$userID = Session::getUserID(); $userID = Session::getUserID();
$db = DB::db(); $db = DB::db();
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
$isSecretBoard = Util::getPost('isSecret', 'bool', false); $isSecretBoard = Util::getPost('isSecret', 'bool', false);
$title = Util::getPost('title'); $title = Util::getPost('title');
@@ -67,7 +68,7 @@ $icon = GetImageURL($me['imgsvr'], $me['picture']);
$db->insert('board', [ $db->insert('board', [
'nation_no'=>$me['nation'], 'nation_no'=>$me['nation'],
'is_secret'=>$isSecretBoard, 'is_secret'=>$isSecretBoard,
'date'=>TimeUtil::now(), 'date'=>$clock->formatNow(),
'general_no'=>$me['no'], 'general_no'=>$me['no'],
'author'=>$me['name'], 'author'=>$me['name'],
'author_icon'=>$icon, 'author_icon'=>$icon,
@@ -79,4 +80,4 @@ Json::die([
'result'=>true, 'result'=>true,
'reason'=>'success', 'reason'=>'success',
'row_id'=>$db->insertId() 'row_id'=>$db->insertId()
]); ]);
+3 -2
View File
@@ -10,6 +10,7 @@ $session = Session::requireGameLogin()->setReadOnly();
$userID = Session::getUserID(); $userID = Session::getUserID();
$db = DB::db(); $db = DB::db();
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
$articleNo = Util::getPost('articleNo', 'int'); $articleNo = Util::getPost('articleNo', 'int');
$text = Util::getPost('text'); $text = Util::getPost('text');
@@ -73,7 +74,7 @@ else if ($isSecretBoard && $permission < 2) {
$db->insert('comment', [ $db->insert('comment', [
'nation_no'=>$me['nation'], 'nation_no'=>$me['nation'],
'is_secret'=>$isSecretBoard, 'is_secret'=>$isSecretBoard,
'date'=>TimeUtil::now(), 'date'=>$clock->formatNow(),
'document_no'=>$articleNo, 'document_no'=>$articleNo,
'general_no'=>$me['no'], 'general_no'=>$me['no'],
'author'=>$me['name'], 'author'=>$me['name'],
@@ -84,4 +85,4 @@ Json::die([
'result'=>true, 'result'=>true,
'reason'=>'success', 'reason'=>'success',
'row_id'=>$db->insertId() 'row_id'=>$db->insertId()
]); ]);
+2 -2
View File
@@ -83,7 +83,7 @@ else{
} }
$now = new \DateTime(); $now = Message::gameNow();
$unlimited = new \DateTime('9999-12-31'); $unlimited = new \DateTime('9999-12-31');
if(in_array($stateOpt, ['try_destroy_src', 'try_destroy_dest'])){ if(in_array($stateOpt, ['try_destroy_src', 'try_destroy_dest'])){
@@ -138,4 +138,4 @@ Json::die([
'result'=>true, 'result'=>true,
'reason'=>'success', 'reason'=>'success',
'state'=>$lastState 'state'=>$lastState
]); ]);
+2 -2
View File
@@ -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']); $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']); $dest = new MessageTarget(0, '', $srcNation['nation'], $srcNation['name'], $srcNation['color']);
$now = new \DateTime(); $now = Message::gameNow();
$unlimited = new \DateTime('9999-12-31'); $unlimited = new \DateTime('9999-12-31');
if($isAgree){ if($isAgree){
@@ -133,4 +133,4 @@ $msgID = $msg->send();
Json::die([ Json::die([
'result'=>true, 'result'=>true,
'reason'=>'success' 'reason'=>'success'
]); ]);
+2 -2
View File
@@ -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']); $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']); $dest = new MessageTarget(0, '', $destNation['nation'], $destNation['name'], $destNation['color']);
$now = new \DateTime(); $now = Message::gameNow();
$unlimited = new \DateTime('9999-12-31'); $unlimited = new \DateTime('9999-12-31');
$aux['reason'] = [ $aux['reason'] = [
@@ -91,4 +91,4 @@ $msgID = $msg->send();
Json::die([ Json::die([
'result'=>true, 'result'=>true,
'reason'=>'success' 'reason'=>'success'
]); ]);
+5 -3
View File
@@ -140,6 +140,8 @@ else{
$me['icon'] = GetImageURL($me['imgsvr'], $me['picture']); $me['icon'] = GetImageURL($me['imgsvr'], $me['picture']);
$clock = GameClock::fromStorage($gameStor);
$gameNow = $clock->nowDateTime();
$db->insert('ng_diplomacy', [ $db->insert('ng_diplomacy', [
'src_nation_id'=>$srcNation['nation'], 'src_nation_id'=>$srcNation['nation'],
'dest_nation_id'=>$destNation['nation'], 'dest_nation_id'=>$destNation['nation'],
@@ -147,7 +149,7 @@ $db->insert('ng_diplomacy', [
'state'=>'proposed', 'state'=>'proposed',
'text_brief'=>$textBrief, 'text_brief'=>$textBrief,
'text_detail'=>$textDetail, 'text_detail'=>$textDetail,
'date'=>TimeUtil::now(), 'date'=>TimeUtil::format($gameNow),
'src_signer'=>$me['no'], 'src_signer'=>$me['no'],
'dest_signer'=>null, 'dest_signer'=>null,
'aux'=>Json::encode([ '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']); $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']); $dest = new MessageTarget(0, '', $destNation['nation'], $destNation['name'], $destNation['color']);
$now = new \DateTime(); $now = \DateTime::createFromImmutable($gameNow);
$unlimited = new \DateTime('9999-12-31'); $unlimited = new \DateTime('9999-12-31');
$josaYi = JosaUtil::pick($newLetterNo, '이'); $josaYi = JosaUtil::pick($newLetterNo, '이');
@@ -195,4 +197,4 @@ Json::die([
'result'=>true, 'result'=>true,
'reason'=>'success', 'reason'=>'success',
'row_id'=>$db->insertId() 'row_id'=>$db->insertId()
]); ]);
+6 -2
View File
@@ -79,7 +79,9 @@ if($token && !$refresh){
'pick'=>Json::decode($token['pick_result']), 'pick'=>Json::decode($token['pick_result']),
'pickMoreFrom'=>$clock->formatTick($pickMoreFrom), 'pickMoreFrom'=>$clock->formatTick($pickMoreFrom),
'pickMoreSeconds'=>intdiv($pickMoreFrom - $now, $clock->ticksPerSecond()), '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, 'pick'=>$pickResult,
'pickMoreFrom'=>$clock->formatTick(($inserted===-1)?$pickMoreFrom:$now), 'pickMoreFrom'=>$clock->formatTick(($inserted===-1)?$pickMoreFrom:$now),
'pickMoreSeconds'=>($inserted===-1)?$pickMoreSecond:0, 'pickMoreSeconds'=>($inserted===-1)?$pickMoreSecond:0,
'validUntil'=>$clock->formatTick($validUntil) 'validUntil'=>$clock->formatTick($validUntil),
'validForSeconds'=>max(0, intdiv($validUntil - $now, $clock->ticksPerSecond())),
'clockMode'=>$clock->getMode(),
]); ]);
+7 -3
View File
@@ -86,7 +86,9 @@ if($tokens){
Json::die([ Json::die([
'result'=>true, 'result'=>true,
'pick'=>$pick, '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([ Json::die([
'result'=>true, 'result'=>true,
'pick'=>$pick, '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(),
]);
+1 -1
View File
@@ -290,7 +290,7 @@ function do추방(General $general, int $myOfficerLevel): ?string
$src, $src,
$src, $src,
$str, $str,
new \DateTime(), Message::gameNow(),
new \DateTime('9999-12-31'), new \DateTime('9999-12-31'),
[] []
); );
+1
View File
@@ -76,6 +76,7 @@ $admin['maxUserCnt'] = $admin['maxgeneral'];
$admin['npcMode'] = $admin['npcmode']; $admin['npcMode'] = $admin['npcmode'];
$admin['turnTerm'] = $admin['turnterm']; $admin['turnTerm'] = $admin['turnterm'];
$admin['isUnited'] = $admin['isunited']; $admin['isUnited'] = $admin['isunited'];
$admin['isOpen'] = $clock->nowTick() >= Util::toInt($admin['opentime']);
$admin['starttime'] = substr($clock->formatTick(Util::toInt($admin['opentime'])), 5, 11); $admin['starttime'] = substr($clock->formatTick(Util::toInt($admin['opentime'])), 5, 11);
$admin['turntime'] = substr($clock->formatTick(Util::toInt($admin['turntime'])), 5, 11); $admin['turntime'] = substr($clock->formatTick(Util::toInt($admin['turntime'])), 5, 11);
unset($admin['npcmode']); unset($admin['npcmode']);
+6 -3
View File
@@ -64,6 +64,7 @@ if ($permission < 3) {
function applyNationPolicy($policy, $nationID, $generalName): ?string function applyNationPolicy($policy, $nationID, $generalName): ?string
{ {
$db = DB::db(); $db = DB::db();
$gameNow = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow();
$nationStor = KVStorage::getStorage($db, $nationID, 'nation_env'); $nationStor = KVStorage::getStorage($db, $nationID, 'nation_env');
$defaultPolicy = AutorunNationPolicy::$defaultPolicy; $defaultPolicy = AutorunNationPolicy::$defaultPolicy;
@@ -139,7 +140,7 @@ function applyNationPolicy($policy, $nationID, $generalName): ?string
$nationPolicyRoot['values'] = $nationPolicy; $nationPolicyRoot['values'] = $nationPolicy;
$nationPolicyRoot['valueSetter'] = $generalName; $nationPolicyRoot['valueSetter'] = $generalName;
$nationPolicyRoot['valueSetTime'] = TimeUtil::now(); $nationPolicyRoot['valueSetTime'] = $gameNow;
$nationStor->npc_nation_policy = $nationPolicyRoot; $nationStor->npc_nation_policy = $nationPolicyRoot;
return null; return null;
} }
@@ -147,6 +148,7 @@ function applyNationPolicy($policy, $nationID, $generalName): ?string
function applyNationPriority($priority, $nationID, $generalName): ?string function applyNationPriority($priority, $nationID, $generalName): ?string
{ {
$db = DB::db(); $db = DB::db();
$gameNow = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow();
$nationStor = KVStorage::getStorage($db, $nationID, 'nation_env'); $nationStor = KVStorage::getStorage($db, $nationID, 'nation_env');
$nationPolicyRoot = $nationStor->npc_nation_policy; $nationPolicyRoot = $nationStor->npc_nation_policy;
@@ -158,7 +160,7 @@ function applyNationPriority($priority, $nationID, $generalName): ?string
} }
$nationPolicyRoot['priority'] = $priority; $nationPolicyRoot['priority'] = $priority;
$nationPolicyRoot['prioritySetter'] = $generalName; $nationPolicyRoot['prioritySetter'] = $generalName;
$nationPolicyRoot['prioritySetTime'] = TimeUtil::now(); $nationPolicyRoot['prioritySetTime'] = $gameNow;
$nationStor->npc_nation_policy = $nationPolicyRoot; $nationStor->npc_nation_policy = $nationPolicyRoot;
return null; return null;
} }
@@ -166,6 +168,7 @@ function applyNationPriority($priority, $nationID, $generalName): ?string
function applyGeneralPriority($priority, $nationID, $generalName): ?string function applyGeneralPriority($priority, $nationID, $generalName): ?string
{ {
$db = DB::db(); $db = DB::db();
$gameNow = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow();
$nationStor = KVStorage::getStorage($db, $nationID, 'nation_env'); $nationStor = KVStorage::getStorage($db, $nationID, 'nation_env');
$generalPolicyRoot = $nationStor->npc_general_policy; $generalPolicyRoot = $nationStor->npc_general_policy;
@@ -206,7 +209,7 @@ function applyGeneralPriority($priority, $nationID, $generalName): ?string
$generalPolicyRoot['priority'] = $priority; $generalPolicyRoot['priority'] = $priority;
$generalPolicyRoot['prioritySetter'] = $generalName; $generalPolicyRoot['prioritySetter'] = $generalName;
$generalPolicyRoot['prioritySetTime'] = TimeUtil::now(); $generalPolicyRoot['prioritySetTime'] = $gameNow;
$nationStor->npc_general_policy = $generalPolicyRoot; $nationStor->npc_general_policy = $generalPolicyRoot;
return null; return null;
} }
+4 -3
View File
@@ -10,7 +10,6 @@ use sammo\GameConst;
use sammo\GameClock; use sammo\GameClock;
use sammo\Json; use sammo\Json;
use sammo\KVStorage; use sammo\KVStorage;
use sammo\TimeUtil;
use sammo\Util; use sammo\Util;
use function sammo\cutTurn; use function sammo\cutTurn;
@@ -33,6 +32,7 @@ class GetReservedCommand extends \sammo\BaseAPI
$commandList = []; $commandList = [];
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
$clock = GameClock::fromStorage($gameStor);
$generalID = $session->generalID; $generalID = $session->generalID;
$invalidTurnList = 0; $invalidTurnList = 0;
@@ -85,11 +85,12 @@ class GetReservedCommand extends \sammo\BaseAPI
return [ return [
'result' => true, 'result' => true,
'turnTimeTick' => Util::toInt($turnTime), 'turnTimeTick' => Util::toInt($turnTime),
'turnTime' => GameClock::fromStorage($gameStor)->formatTick(Util::toInt($turnTime)), 'turnTime' => $clock->formatTick(Util::toInt($turnTime)),
'turnTerm' => $turnTerm, 'turnTerm' => $turnTerm,
'year' => $year, 'year' => $year,
'month' => $month, 'month' => $month,
'date' => GameClock::fromStorage($gameStor)->formatTick(GameClock::fromStorage($gameStor)->nowTick(), true), 'date' => $clock->formatTick($clock->nowTick(), true),
'clockMode' => $clock->getMode(),
'turn' => $commandList, 'turn' => $commandList,
'autorun_limit' => $generalAux['autorun_limit'] ?? null, 'autorun_limit' => $generalAux['autorun_limit'] ?? null,
]; ];
+3 -3
View File
@@ -21,7 +21,6 @@ use sammo\LastTurn;
use sammo\Validator; use sammo\Validator;
use sammo\Session; use sammo\Session;
use sammo\TimeUtil;
use sammo\Util; use sammo\Util;
use function sammo\buildNationCommandClass; use function sammo\buildNationCommandClass;
@@ -185,8 +184,9 @@ class GetFrontInfo extends \sammo\BaseAPI
$lastVote = null; $lastVote = null;
if ($lastVoteID) { if ($lastVoteID) {
$voteStor = KVStorage::getStorage($db, 'vote'); $voteStor = KVStorage::getStorage($db, 'vote');
$lastVote = VoteInfo::fromArray($voteStor->getValue("vote_{$lastVoteID}")); $rawLastVote = VoteInfo::normalizeGameStorage($voteStor->getValue("vote_{$lastVoteID}"), $clock);
if ($lastVote->endDate && $lastVote->endDate < TimeUtil::now()) { $lastVote = VoteInfo::fromGameStorage($rawLastVote, $clock);
if ($rawLastVote['endTick'] !== null && $rawLastVote['endTick'] < $clock->nowTick()) {
$lastVote = null; $lastVote = null;
} }
} }
@@ -10,7 +10,6 @@ use sammo\Enums\RankColumn;
use sammo\GameConst; use sammo\GameConst;
use sammo\General; use sammo\General;
use sammo\KVStorage; use sammo\KVStorage;
use sammo\TimeUtil;
use sammo\UserLogger; use sammo\UserLogger;
class BuyRandomUnique extends \sammo\BaseAPI class BuyRandomUnique extends \sammo\BaseAPI
@@ -56,7 +55,7 @@ class BuyRandomUnique extends \sammo\BaseAPI
$userLogger->push("{$reqAmount} 포인트로 랜덤 유니크 구입", "inheritPoint"); $userLogger->push("{$reqAmount} 포인트로 랜덤 유니크 구입", "inheritPoint");
$userLogger->flush(); $userLogger->flush();
$general->setAuxVar('inheritRandomUnique', TimeUtil::now()); $general->setAuxVar('inheritRandomUnique', true);
$inheritStor->setValue('previous', [$previousPoint - $reqAmount, null]); $inheritStor->setValue('previous', [$previousPoint - $reqAmount, null]);
$general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, $reqAmount); $general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, $reqAmount);
$general->applyDB($db); $general->applyDB($db);
+2 -2
View File
@@ -116,7 +116,7 @@ class CheckOwner extends \sammo\BaseAPI
$src, $src,
$dest, $dest,
"{$destGeneralName}의 소유자는 {$destGeneralOwnerName} 입니다.", "{$destGeneralName}의 소유자는 {$destGeneralOwnerName} 입니다.",
new \DateTime(), Message::gameNow(),
new \DateTime('9999-12-31'), new \DateTime('9999-12-31'),
[] []
); );
@@ -142,7 +142,7 @@ class CheckOwner extends \sammo\BaseAPI
$src, $src,
$dest, $dest,
"소유자명이 누군가에 의해 확인되었습니다.", "소유자명이 누군가에 의해 확인되었습니다.",
new \DateTime(), Message::gameNow(),
new \DateTime('9999-12-31'), new \DateTime('9999-12-31'),
[] []
); );
+4 -4
View File
@@ -44,7 +44,7 @@ class SendMessage extends \sammo\BaseAPI
private function genPublicMessage(MessageTarget $src, string $text): Message private function genPublicMessage(MessageTarget $src, string $text): Message
{ {
$now = new \DateTime(); $now = Message::gameNow();
$unlimited = new \DateTime('9999-12-31'); $unlimited = new \DateTime('9999-12-31');
$msg = new Message( $msg = new Message(
@@ -62,7 +62,7 @@ class SendMessage extends \sammo\BaseAPI
private function genNationalMessage(MessageTarget $src, string $text): Message private function genNationalMessage(MessageTarget $src, string $text): Message
{ {
$now = new \DateTime(); $now = Message::gameNow();
$unlimited = new \DateTime('9999-12-31'); $unlimited = new \DateTime('9999-12-31');
$dest = new MessageTarget(0, '', $src->nationID, $src->nationName, $src->color); $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 private function genDiplomacyMessage(MessageTarget $src, int $destNationID, string $text): Message|string
{ {
$now = new \DateTime(); $now = Message::gameNow();
$unlimited = new \DateTime('9999-12-31'); $unlimited = new \DateTime('9999-12-31');
$destNation = getNationStaticInfo($destNationID); $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 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'); $unlimited = new \DateTime('9999-12-31');
$db = DB::db(); $db = DB::db();
+3 -2
View File
@@ -6,8 +6,8 @@ use sammo\Session;
use DateTimeInterface; use DateTimeInterface;
use sammo\DB; use sammo\DB;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\GameClock;
use sammo\KVStorage; use sammo\KVStorage;
use sammo\TimeUtil;
use sammo\Validator; use sammo\Validator;
use sammo\WebUtil; use sammo\WebUtil;
@@ -51,8 +51,9 @@ class SetNotice extends \sammo\BaseAPI
$nationID = $me['nation']; $nationID = $me['nation'];
$nationStor = KVStorage::getStorage($db, $nationID, 'nation_env'); $nationStor = KVStorage::getStorage($db, $nationID, 'nation_env');
$gameNow = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow();
$nationStor->nationNotice = [ $nationStor->nationNotice = [
'date'=>TimeUtil::now(), 'date'=>$gameNow,
'msg'=>WebUtil::htmlPurify($msg), 'msg'=>WebUtil::htmlPurify($msg),
'author'=>$me['name'], 'author'=>$me['name'],
'authorID'=>$me['no'], 'authorID'=>$me['no'],
@@ -12,7 +12,6 @@ use sammo\GameClock;
use sammo\General; use sammo\General;
use sammo\Json; use sammo\Json;
use sammo\KVStorage; use sammo\KVStorage;
use sammo\TimeUtil;
use sammo\Util; use sammo\Util;
use function sammo\checkLimit; use function sammo\checkLimit;
@@ -40,6 +39,7 @@ class GetReservedCommand extends \sammo\BaseAPI
increaseRefresh("사령부", 1); increaseRefresh("사령부", 1);
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
$clock = GameClock::fromStorage($gameStor);
$userID = $session->userID; $userID = $session->userID;
$me = $db->queryFirstRow( $me = $db->queryFirstRow(
@@ -158,7 +158,8 @@ class GetReservedCommand extends \sammo\BaseAPI
'year' => $year, 'year' => $year,
'month' => $month, 'month' => $month,
'turnTerm' => $turnTerm, 'turnTerm' => $turnTerm,
'date' => TimeUtil::now(true), 'date' => $clock->formatTick($clock->nowTick(), true),
'clockMode' => $clock->getMode(),
'chiefList' => $nationChiefList, 'chiefList' => $nationChiefList,
'troopList' => $troopList, 'troopList' => $troopList,
'isChief' => ($me['officer_level'] > 4), 'isChief' => ($me['officer_level'] > 4),
+4 -3
View File
@@ -10,8 +10,9 @@ use sammo\Enums\GeneralLiteQueryMode;
use sammo\Enums\GeneralQueryMode; use sammo\Enums\GeneralQueryMode;
use sammo\General; use sammo\General;
use sammo\GeneralLite; use sammo\GeneralLite;
use sammo\GameClock;
use sammo\KVStorage;
use sammo\Session; use sammo\Session;
use sammo\TimeUtil;
use sammo\Validator; use sammo\Validator;
class AddComment extends \sammo\BaseAPI class AddComment extends \sammo\BaseAPI
@@ -47,7 +48,8 @@ class AddComment extends \sammo\BaseAPI
$generalName = $general->getName(); $generalName = $general->getName();
$nationID = $general->getNationID(); $nationID = $general->getNationID();
$nationName = $general->getStaticNation()['name']; $nationName = $general->getStaticNation()['name'];
$date = TimeUtil::now(); $db = DB::db();
$date = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->formatNow();
$comment = new VoteComment( $comment = new VoteComment(
@@ -61,7 +63,6 @@ class AddComment extends \sammo\BaseAPI
date: $date date: $date
); );
$db = DB::db();
$db->insert('vote_comment', $comment->toArray()); $db->insert('vote_comment', $comment->toArray());
return null; return null;
+6 -1
View File
@@ -8,6 +8,7 @@ use sammo\DB;
use sammo\DTO\VoteComment; use sammo\DTO\VoteComment;
use sammo\DTO\VoteInfo; use sammo\DTO\VoteInfo;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\GameClock;
use sammo\Json; use sammo\Json;
use sammo\KVStorage; use sammo\KVStorage;
use sammo\Validator; use sammo\Validator;
@@ -35,13 +36,16 @@ class GetVoteDetail extends \sammo\BaseAPI
{ {
$voteID = $this->args['voteID']; $voteID = $this->args['voteID'];
$db = DB::db(); $db = DB::db();
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
$voteStor = KVStorage::getStorage($db, 'vote'); $voteStor = KVStorage::getStorage($db, 'vote');
$rawVote = $voteStor->getValue("vote_{$voteID}"); $rawVote = $voteStor->getValue("vote_{$voteID}");
if (!$rawVote) { if (!$rawVote) {
return '설문조사가 없습니다.'; 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( $votes = array_map(fn ($arr) => [Json::decode($arr[0]), $arr[1]], $db->queryAllLists(
@@ -70,6 +74,7 @@ class GetVoteDetail extends \sammo\BaseAPI
'comments' => $comments, 'comments' => $comments,
'myVote' => $myVote, 'myVote' => $myVote,
'userCnt' => $userCnt, 'userCnt' => $userCnt,
'isOpen' => $isOpen,
]; ];
} }
} }
+5 -3
View File
@@ -6,6 +6,7 @@ use DateTimeInterface;
use sammo\DB; use sammo\DB;
use sammo\DTO\VoteInfo; use sammo\DTO\VoteInfo;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\GameClock;
use sammo\KVStorage; use sammo\KVStorage;
use sammo\Session; 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 public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType
{ {
$db = DB::db(); $db = DB::db();
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
$voteStor = KVStorage::getStorage($db, 'vote'); $voteStor = KVStorage::getStorage($db, 'vote');
$votes = []; $votes = [];
foreach($voteStor->getAll() as $voteKey => $rawVote){ foreach($voteStor->getAll() as $voteKey => $rawVote){
if(!str_starts_with($voteKey, 'vote_')){ if(preg_match('/^vote_(\d+)$/D', $voteKey, $matches) !== 1){
continue; continue;
} }
$voteID = (int)substr($voteKey, 5); $voteID = (int)$matches[1];
$votes[$voteID] = VoteInfo::fromArray($rawVote); $votes[$voteID] = VoteInfo::fromGameStorage($rawVote, $clock);
} }
return [ return [
+19 -15
View File
@@ -9,7 +9,7 @@ use sammo\Enums\APIRecoveryType;
use sammo\KVStorage; use sammo\KVStorage;
use sammo\RootDB; use sammo\RootDB;
use sammo\Session; use sammo\Session;
use sammo\TimeUtil; use sammo\GameClock;
use sammo\Util; use sammo\Util;
use sammo\Validator; use sammo\Validator;
@@ -37,7 +37,7 @@ class NewVote extends \sammo\BaseAPI
return null; return null;
} }
function closeOldVote(int $voteID, KVStorage $voteStor) function closeOldVote(int $voteID, KVStorage $voteStor, GameClock $clock)
{ {
$db = DB::db(); $db = DB::db();
$voteStor = KVStorage::getStorage($db, 'vote'); $voteStor = KVStorage::getStorage($db, 'vote');
@@ -45,13 +45,14 @@ class NewVote extends \sammo\BaseAPI
if (!$rawLastVoteInfo) { if (!$rawLastVoteInfo) {
return; return;
} }
$lastVoteInfo = VoteInfo::fromArray($rawLastVoteInfo); $rawLastVoteInfo = VoteInfo::normalizeGameStorage($rawLastVoteInfo, $clock);
if ($lastVoteInfo->endDate) { if ($rawLastVoteInfo['endTick'] !== null) {
return; return;
} }
$lastVoteInfo->endDate = TimeUtil::now(); $rawLastVoteInfo['endTick'] = $clock->nowTick();
$voteStor->setValue("vote_{$voteID}", $lastVoteInfo->toArray()); $rawLastVoteInfo['endDate'] = $clock->formatTick($rawLastVoteInfo['endTick']);
$voteStor->setValue("vote_{$voteID}", $rawLastVoteInfo);
} }
function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType
@@ -71,7 +72,11 @@ class NewVote extends \sammo\BaseAPI
$multipleOptions = 0; $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 */ /** @var ?string */
$endDate = $this->args['endDate'] ?? null; $endDate = $this->args['endDate'] ?? null;
/** @var string[] */ /** @var string[] */
@@ -83,9 +88,9 @@ class NewVote extends \sammo\BaseAPI
if($endDate !== null){ if($endDate !== null){
try{ try{
$oNow = new \DateTimeImmutable($now);
$oEndDate = new \DateTimeImmutable($endDate); $oEndDate = new \DateTimeImmutable($endDate);
if($oEndDate < $oNow){ $endTick = $clock->dateTimeToTick($oEndDate);
if($endTick < $nowTick){
return '종료일이 이미 지났습니다.'; return '종료일이 이미 지났습니다.';
} }
} }
@@ -96,17 +101,13 @@ class NewVote extends \sammo\BaseAPI
$userName = $session->userName; $userName = $session->userName;
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$lastVote = $gameStor->getValue('lastVote') ?? 0; $lastVote = $gameStor->getValue('lastVote') ?? 0;
$voteID = $lastVote + 1; $voteID = $lastVote + 1;
$voteStor = KVStorage::getStorage($db, 'vote'); $voteStor = KVStorage::getStorage($db, 'vote');
if (!($this->args['keepOldVote'] ?? false)) { if (!($this->args['keepOldVote'] ?? false)) {
$this->closeOldVote($lastVote, $voteStor); $this->closeOldVote($lastVote, $voteStor, $clock);
} }
$multipleOptions = Util::valueFit($multipleOptions, 0, count($options)); $multipleOptions = Util::valueFit($multipleOptions, 0, count($options));
@@ -122,7 +123,10 @@ class NewVote extends \sammo\BaseAPI
options: $options, 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); $gameStor->setValue('lastVote', $voteID);
$db->update('general', [ $db->update('general', [
+5 -2
View File
@@ -8,6 +8,7 @@ use sammo\DTO\VoteInfo;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\Enums\GeneralQueryMode; use sammo\Enums\GeneralQueryMode;
use sammo\General; use sammo\General;
use sammo\GameClock;
use sammo\Json; use sammo\Json;
use sammo\KVStorage; use sammo\KVStorage;
use sammo\LiteHashDRBG; use sammo\LiteHashDRBG;
@@ -54,15 +55,17 @@ class Vote extends \sammo\BaseAPI
return '선택한 항목이 없습니다.'; return '선택한 항목이 없습니다.';
} }
$db = DB::db(); $db = DB::db();
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
$voteStor = KVStorage::getStorage($db, 'vote'); $voteStor = KVStorage::getStorage($db, 'vote');
$rawVoteInfo = $voteStor->getValue("vote_{$voteID}"); $rawVoteInfo = $voteStor->getValue("vote_{$voteID}");
if (!$rawVoteInfo) { if (!$rawVoteInfo) {
return '설문조사가 없습니다.'; 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 '설문조사가 종료되었습니다.'; return '설문조사가 종료되었습니다.';
} }
+1 -1
View File
@@ -193,7 +193,7 @@ class che_몰수 extends Command\NationCommand
$src, $src,
$src, $src,
$text, $text,
new \DateTime(), Message::gameNow(),
new \DateTime('9999-12-31'), new \DateTime('9999-12-31'),
[] []
); );
@@ -198,8 +198,9 @@ class che_불가침제의 extends Command\NationCommand
$destNation['color'] $destNation['color']
); );
$now = new \DateTime($date); $clock = \sammo\GameClock::fromStorage(\sammo\KVStorage::getStorage($db, 'game_env'));
$validUntil = new \DateTime($date); $now = \DateTime::createFromImmutable($clock->tickToDateTime($general->getTurnTick()));
$validUntil = clone $now;
$validMinutes = max(30, $env['turnterm'] * 3); $validMinutes = max(30, $env['turnterm'] * 3);
$validUntil->add(new \DateInterval("PT{$validMinutes}M")); $validUntil->add(new \DateInterval("PT{$validMinutes}M"));
@@ -147,8 +147,9 @@ class che_불가침파기제의 extends Command\NationCommand{
$destNation['color'] $destNation['color']
); );
$now = new \DateTime($date); $clock = \sammo\GameClock::fromStorage(\sammo\KVStorage::getStorage($db, 'game_env'));
$validUntil = new \DateTime($date); $now = \DateTime::createFromImmutable($clock->tickToDateTime($general->getTurnTick()));
$validUntil = clone $now;
$validMinutes = max(30, $env['turnterm']*3); $validMinutes = max(30, $env['turnterm']*3);
$validUntil->add(new \DateInterval("PT{$validMinutes}M")); $validUntil->add(new \DateInterval("PT{$validMinutes}M"));
@@ -217,4 +218,4 @@ class che_불가침파기제의 extends Command\NationCommand{
], ],
]; ];
} }
} }
@@ -145,8 +145,9 @@ class che_종전제의 extends Command\NationCommand{
$destNation['color'] $destNation['color']
); );
$now = new \DateTime($date); $clock = \sammo\GameClock::fromStorage(\sammo\KVStorage::getStorage($db, 'game_env'));
$validUntil = new \DateTime($date); $now = \DateTime::createFromImmutable($clock->tickToDateTime($general->getTurnTick()));
$validUntil = clone $now;
$validMinutes = max(30, $env['turnterm']*3); $validMinutes = max(30, $env['turnterm']*3);
$validUntil->add(new \DateInterval("PT{$validMinutes}M")); $validUntil->add(new \DateInterval("PT{$validMinutes}M"));
@@ -203,4 +204,4 @@ class che_종전제의 extends Command\NationCommand{
], ],
]; ];
} }
} }
+33
View File
@@ -2,8 +2,41 @@
namespace sammo\DTO; namespace sammo\DTO;
use sammo\GameClock;
use sammo\Util;
class VoteInfo extends \LDTO\DTO class VoteInfo extends \LDTO\DTO
{ {
/**
* 기존 문자열만 가진 vote도 읽되, 저장 경계에서는 반드시 tick을 함께 둡니다.
*
* @return array<string,mixed>
*/
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 function __construct(
public int $id, public int $id,
public string $title, public string $title,
+2 -2
View File
@@ -215,7 +215,7 @@ class DiplomaticMessage extends Message{
$this->dest, $this->dest,
$this->src, $this->src,
"【외교】{$year}{$month}월: {$this->src->nationName}{$josaYi} {$this->dest->nationName}에게 제안한 {$this->diplomacyDetail}", "【외교】{$year}{$month}월: {$this->src->nationName}{$josaYi} {$this->dest->nationName}에게 제안한 {$this->diplomacyDetail}",
new \DateTime(), Message::gameNow(),
new \DateTime('9999-12-31'), new \DateTime('9999-12-31'),
[ [
'delete'=>$this->id, 'delete'=>$this->id,
@@ -231,7 +231,7 @@ class DiplomaticMessage extends Message{
$this->dest, $this->dest,
$this->src, $this->src,
"【외교】{$year}{$month}월: {$this->src->nationName}{$josaYi} {$this->dest->nationName}에게 제안한 {$this->diplomacyDetail}", "【외교】{$year}{$month}월: {$this->src->nationName}{$josaYi} {$this->dest->nationName}에게 제안한 {$this->diplomacyDetail}",
new \DateTime(), Message::gameNow(),
new \DateTime('9999-12-31'), new \DateTime('9999-12-31'),
[ [
'delete'=>$this->id, 'delete'=>$this->id,
+1 -1
View File
@@ -123,7 +123,7 @@ class OpenNationBetting extends \sammo\Event\Action
} }
$logger->flush(); $logger->flush();
$now = new DateTime(); $now = Message::gameNow();
$text = "새로운 {$name} 내기가 열렸습니다. 천통국 베팅란을 확인해주세요."; $text = "새로운 {$name} 내기가 열렸습니다. 천통국 베팅란을 확인해주세요.";
$src = new MessageTarget(0, '', 0, 'System', '#000000'); $src = new MessageTarget(0, '', 0, 'System', '#000000');
+1 -1
View File
@@ -3727,7 +3727,7 @@ class GeneralAI
$src, $src,
$src, $src,
$general->getVar('npcmsg'), $general->getVar('npcmsg'),
new \DateTime(), Message::gameNow(),
new \DateTime('9999-12-31'), new \DateTime('9999-12-31'),
[] []
); );
+24 -5
View File
@@ -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 public function setSentInfo(int $mailbox, int $messageID) : self
{ {
if(!Message::isValidMailBox($mailbox)){ if(!Message::isValidMailBox($mailbox)){
@@ -78,6 +84,15 @@ class Message
} }
public function toArray():array{ 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){ if($this->msgType === MessageType::public){
$src = $this->src->toArray(); $src = $this->src->toArray();
$dest = null; $dest = null;
@@ -98,7 +113,9 @@ class Message
'dest'=>$dest, 'dest'=>$dest,
'text'=>$this->msg, 'text'=>$this->msg,
'option'=>$this->msgOption, '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, $src,
$dest, $dest,
$msg, $msg,
new \DateTime(), self::gameNow(),
new \DateTime('9999-12-31'), new \DateTime('9999-12-31'),
[] []
); );
@@ -480,6 +497,8 @@ class Message
} }
public function invalidate(?array $newMsgOption=null, bool $hideMsg=true){ 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){ if($newMsgOption !== null){
$this->msgOption = $newMsgOption; $this->msgOption = $newMsgOption;
} }
@@ -487,7 +506,8 @@ class Message
$this->msgOption['invalid'] = true; $this->msgOption['invalid'] = true;
if($hideMsg){ if($hideMsg){
$this->validUntil = new \DateTime('2000-12-31'); $validUntilTick = GameClock::addTicks($clock->nowTick(), -1);
$this->validUntil = \DateTime::createFromImmutable($clock->tickToDateTime($validUntilTick));
} }
else{ else{
if(key_exists('receiverMessageID', $this->msgOption)){ if(key_exists('receiverMessageID', $this->msgOption)){
@@ -505,8 +525,7 @@ class Message
'text' => $this->msg, 'text' => $this->msg,
'option' => $this->msgOption 'option' => $this->msgOption
]), ]),
'valid_until'=>GameClock::fromStorage(KVStorage::getStorage($db, 'game_env')) 'valid_until'=>$validUntilTick,
->dateTimeToTick($this->validUntil),
], 'id=%i', $this->id); ], 'id=%i', $this->id);
} }
+1 -1
View File
@@ -142,7 +142,7 @@ class RaiseInvaderMessage extends Message
$srcTarget = MessageTarget::buildSystemTarget(); $srcTarget = MessageTarget::buildSystemTarget();
$destTarget = MessageTarget::buildQuick($destGeneralID); $destTarget = MessageTarget::buildQuick($destGeneralID);
if ($date === null) { if ($date === null) {
$date = new \DateTime(); $date = Message::gameNow();
} }
/** /**
+5 -5
View File
@@ -108,7 +108,7 @@ class ScoutMessage extends Message
$this->src, $this->src,
$this->dest, $this->dest,
"{$this->src->nationName}{$josaRo} 등용 제의 수락", "{$this->src->nationName}{$josaRo} 등용 제의 수락",
new \DateTime(), Message::gameNow(),
new \DateTime('9999-12-31'), new \DateTime('9999-12-31'),
[ [
'delete' => $this->id 'delete' => $this->id
@@ -156,7 +156,7 @@ class ScoutMessage extends Message
$this->src, $this->src,
$this->dest, $this->dest,
"{$this->src->nationName}{$josaRo} 등용 제의 거부", "{$this->src->nationName}{$josaRo} 등용 제의 거부",
new \DateTime(), Message::gameNow(),
new \DateTime('9999-12-31'), new \DateTime('9999-12-31'),
[ [
'delete' => $this->id 'delete' => $this->id
@@ -205,9 +205,9 @@ class ScoutMessage extends Message
$db = DB::db(); $db = DB::db();
$srcGeneral = $db->queryFirstRow('SELECT `name`, nation FROM general WHERE `no`=%i', $srcGeneralID); $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); $destGeneral = $db->queryFirstRow('SELECT `name`, nation, `officer_level` FROM general WHERE `no`=%i', $destGeneralID);
if ($date === null) { if ($date === null) {
$date = new \DateTime(); $date = Message::gameNow();
} }
if ($destGeneral['officer_level'] == 12) { if ($destGeneral['officer_level'] == 12) {
if ($reason !== null) { if ($reason !== null) {
+25 -11
View File
@@ -6,15 +6,20 @@ use sammo\Enums\EventTarget;
use sammo\Enums\InheritanceKey; use sammo\Enums\InheritanceKey;
use \Symfony\Component\Lock; use \Symfony\Component\Lock;
class TurnExecutionHelper class TurnExecutionHelper
{ {
/** @var General*/ /** @var General*/
protected $generalObj; protected $generalObj;
public function __construct(General $general) public function __construct(General $general)
{ {
$this->generalObj = $general; $this->generalObj = $general;
} }
public static function monotonicCompletionTick(int $completedTick, int $candidateTick): int
{
return max($completedTick, $candidateTick);
}
public function __destruct() public function __destruct()
{ {
@@ -450,9 +455,12 @@ class TurnExecutionHelper
updateTraffic(); updateTraffic();
if ($executionOver) { if ($executionOver) {
if ($currentTurn !== null) { if ($currentTurn !== null) {
$executed = true; $executed = true;
$gameStor->turntime = $currentTurn; $gameStor->turntime = self::monotonicCompletionTick(
Util::toInt($gameStor->turntime),
$currentTurn,
);
} }
unlock(); unlock();
return $gameStor->turntime; return $gameStor->turntime;
@@ -497,10 +505,16 @@ class TurnExecutionHelper
$gameStor->month $gameStor->month
); );
if ($currentTurn !== null) { if ($currentTurn !== null) {
$executed = true; $executed = true;
$gameStor->turntime = $currentTurn; // 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(); processTournament();
+2 -1
View File
@@ -55,7 +55,8 @@ class UserLogger
} }
$db = DB::db(); $db = DB::db();
$date = TimeUtil::now(); $clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
$date = $clock->formatTick($clock->nowTick());
$serverID = UniqueConst::$serverID; $serverID = UniqueConst::$serverID;
$request = array_map(function ($textAndType) use ($date, $serverID) { $request = array_map(function ($textAndType) use ($date, $serverID) {
[$text, $type] = $textAndType; [$text, $type] = $textAndType;
+2 -2
View File
@@ -297,7 +297,7 @@ CREATE TABLE IF NOT EXISTS `ng_old_nations` (
`server_id` CHAR(20) NOT NULL DEFAULT '0', `server_id` CHAR(20) NOT NULL DEFAULT '0',
`nation` INT(11) NOT NULL DEFAULT '0', `nation` INT(11) NOT NULL DEFAULT '0',
`data` LONGTEXT NOT NULL DEFAULT '{}' COLLATE 'utf8mb4_bin', `data` LONGTEXT NOT NULL DEFAULT '{}' COLLATE 'utf8mb4_bin',
`date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `date` DATETIME NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
INDEX `server_id` (`server_id`, `nation`), INDEX `server_id` (`server_id`, `nation`),
CONSTRAINT `json` CHECK (json_valid(`data`)) CONSTRAINT `json` CHECK (json_valid(`data`))
@@ -395,7 +395,7 @@ CREATE TABLE `ng_diplomacy` (
`state` ENUM('proposed', 'activated', 'cancelled', 'replaced') NOT NULL DEFAULT 'proposed', `state` ENUM('proposed', 'activated', 'cancelled', 'replaced') NOT NULL DEFAULT 'proposed',
`text_brief` TEXT NOT NULL, `text_brief` TEXT NOT NULL,
`text_detail` 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, `src_signer` INT(11) NOT NULL,
`dest_signer` INT(11) NULL DEFAULT NULL, `dest_signer` INT(11) NULL DEFAULT NULL,
`aux` TEXT NULL DEFAULT NULL COLLATE 'utf8mb4_bin', `aux` TEXT NULL DEFAULT NULL COLLATE 'utf8mb4_bin',
+3 -1
View File
@@ -42,6 +42,7 @@
:maxTurn="maxChiefTurn" :maxTurn="maxChiefTurn"
:maxPushTurn="Math.floor(maxChiefTurn / 2)" :maxPushTurn="Math.floor(maxChiefTurn / 2)"
:date="date" :date="date"
:clockMode="clockMode"
:officer="officer" :officer="officer"
@raiseReload="reloadTable()" @raiseReload="reloadTable()"
/> />
@@ -139,6 +140,7 @@ const tableObj = reactive<Omit<OptionalFull<ChiefResponse>, "result">>({
month: undefined, month: undefined,
turnTerm: undefined, turnTerm: undefined,
date: undefined, date: undefined,
clockMode: undefined,
troopList: undefined, troopList: undefined,
chiefList: undefined, chiefList: undefined,
isChief: undefined, isChief: undefined,
@@ -149,7 +151,7 @@ const tableObj = reactive<Omit<OptionalFull<ChiefResponse>, "result">>({
unitSet: undefined, 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 { let postFilterNationCommand = function (turnObj: TurnObj): TurnObj {
return turnObj; return turnObj;
+1 -9
View File
@@ -195,7 +195,6 @@ import { onMounted, reactive, ref, watch, computed } from "vue";
import type { VoteInfo, VoteDetailResult } from "@/defs/API/Vote"; import type { VoteInfo, VoteDetailResult } from "@/defs/API/Vote";
import { SammoAPI } from "@/SammoAPI"; import { SammoAPI } from "@/SammoAPI";
import { isString, range, sum } from "lodash-es"; import { isString, range, sum } from "lodash-es";
import { formatTime } from "@/util/formatTime";
import { isBrightColor } from "@/util/isBrightColor"; import { isBrightColor } from "@/util/isBrightColor";
import { formatVoteColor } from "@/utilGame/formatVoteColor"; import { formatVoteColor } from "@/utilGame/formatVoteColor";
@@ -239,14 +238,7 @@ const canVote = computed(() => {
if (currentVote.value.myVote) { if (currentVote.value.myVote) {
return false; return false;
} }
const endDate = currentVote.value.voteInfo.endDate; return currentVote.value.isOpen;
if (endDate) {
const now = formatTime(new Date());
if (now > endDate) {
return false;
}
}
return true;
}); });
const currentVoteID = ref<number>(); const currentVoteID = ref<number>();
+3 -1
View File
@@ -13,7 +13,7 @@
class="col alert alert-primary m-0 p-0" class="col alert alert-primary m-0 p-0"
style="text-align: center; display: flex; justify-content: center; align-items: center" style="text-align: center; display: flex; justify-content: center; align-items: center"
> >
<SimpleClock :serverTime="serverNow" /> <SimpleClock :serverTime="serverNow" :running="clockRunning" />
</div> </div>
<div class="col d-grid"> <div class="col d-grid">
<BDropdown right text="반복"> <BDropdown right text="반복">
@@ -467,6 +467,7 @@ async function pushGeneralCommand(amount: number) {
} }
const serverNow = ref(new Date()); const serverNow = ref(new Date());
const clockRunning = ref(true);
function pushGeneralCommandSingle(e: Event) { function pushGeneralCommandSingle(e: Event) {
//NOTE: split 구현에 버그가 있어서, 수동으로 구분해야함 //NOTE: split 구현에 버그가 있어서, 수동으로 구분해야함
@@ -548,6 +549,7 @@ async function reloadCommandList() {
} }
serverNow.value = parseTime(result.date); serverNow.value = parseTime(result.date);
clockRunning.value = result.clockMode === "realtime";
} }
async function reserveCommandDirect(args: [number[], TurnObj][], reload = true): Promise<boolean> { async function reserveCommandDirect(args: [number[], TurnObj][], reload = true): Promise<boolean> {
+5 -1
View File
@@ -27,7 +27,7 @@
<div class="row gx-1 gy-1 py-1"> <div class="row gx-1 gy-1 py-1">
<div class="col-lg-4 mx-0 mb-0 mt-1 d-grid"> <div class="col-lg-4 mx-0 mb-0 mt-1 d-grid">
<div class="alert alert-primary mb-0 center" style="padding: 0.5rem 0"> <div class="alert alert-primary mb-0 center" style="padding: 0.5rem 0">
<SimpleClock :serverTime="parseTime(props.date)" /> <SimpleClock :serverTime="parseTime(props.date)" :running="props.clockMode === 'realtime'" />
</div> </div>
</div> </div>
@@ -289,6 +289,10 @@ const props = defineProps({
maxTurn: VueTypes.integer.isRequired, maxTurn: VueTypes.integer.isRequired,
maxPushTurn: VueTypes.integer.isRequired, maxPushTurn: VueTypes.integer.isRequired,
date: VueTypes.string.isRequired, date: VueTypes.string.isRequired,
clockMode: {
type: String as PropType<"realtime" | "manual">,
required: true,
},
year: VueTypes.integer.isRequired, year: VueTypes.integer.isRequired,
month: VueTypes.integer.isRequired, month: VueTypes.integer.isRequired,
turnTerm: VueTypes.integer.isRequired, turnTerm: VueTypes.integer.isRequired,
+2 -5
View File
@@ -126,8 +126,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { MsgItem, MsgTarget, MsgType } from "@/defs/API/Message"; import type { MsgItem, MsgTarget, MsgType } from "@/defs/API/Message";
import { parseTime } from "@/util/parseTime";
import { differenceInMilliseconds, addMinutes } from "date-fns/esm";
import { computed, onMounted, ref, toRef, watch, type ComputedRef, type Ref } from "vue"; import { computed, onMounted, ref, toRef, watch, type ComputedRef, type Ref } from "vue";
import linkifyStr from "linkify-string"; import linkifyStr from "linkify-string";
import { SammoAPI } from "@/SammoAPI"; import { SammoAPI } from "@/SammoAPI";
@@ -205,11 +203,10 @@ function testDeletable(msg: MsgItem): boolean {
if (msg.option.invalid) return false; if (msg.option.invalid) return false;
if (!(msg.option.deletable ?? true)) return false; if (!(msg.option.deletable ?? true)) return false;
const now = new Date(); const timeDiff = msg.deleteRemainingMilliseconds;
const last5min = addMinutes(parseTime(msg.time), 5);
const timeDiff = differenceInMilliseconds(last5min, now);
if (timeDiff <= 0) return false; if (timeDiff <= 0) return false;
if (msg.clockMode === "manual") return true;
deletableTimer.value = window.setTimeout(() => { deletableTimer.value = window.setTimeout(() => {
deletable.value = testDeletable(msg); deletable.value = testDeletable(msg);
+27 -7
View File
@@ -4,7 +4,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { addMilliseconds } from "date-fns"; import { addMilliseconds } from "date-fns";
import { type PropType, ref, onMounted, watch } from "vue"; import { type PropType, ref, onMounted, onUnmounted, watch } from "vue";
import { formatTime } from "@/util/formatTime"; import { formatTime } from "@/util/formatTime";
const props = defineProps({ const props = defineProps({
serverTime: { serverTime: {
@@ -17,25 +17,39 @@ const props = defineProps({
required: false, required: false,
default: "HH:mm:ss", default: "HH:mm:ss",
}, },
running: {
type: Boolean,
default: true,
},
}); });
const timeDiff = ref(0); const timeDiff = ref(0);
const serverNow = ref(""); const serverNow = ref("");
watch( watch(
() => props.serverTime, () => [props.serverTime, props.running] as const,
(newValue) => { ([newValue]) => {
const clientNow = new Date(); const clientNow = new Date();
timeDiff.value = newValue.getTime() - clientNow.getTime(); timeDiff.value = newValue.getTime() - clientNow.getTime();
updateNow();
} }
); );
let timer: ReturnType<typeof setTimeout> | undefined;
function updateNow() { function updateNow() {
const serverNowObj = addMilliseconds(new Date(), timeDiff.value); if (timer !== undefined) {
clearTimeout(timer);
timer = undefined;
}
const serverNowObj = props.running
? addMilliseconds(new Date(), timeDiff.value)
: props.serverTime;
serverNow.value = formatTime(serverNowObj, props.timeFormat); serverNow.value = formatTime(serverNowObj, props.timeFormat);
setTimeout(() => { if (props.running) {
updateNow(); timer = setTimeout(() => {
}, 1000 - serverNowObj.getMilliseconds()); updateNow();
}, 1000 - serverNowObj.getMilliseconds());
}
} }
onMounted(() => { onMounted(() => {
@@ -43,4 +57,10 @@ onMounted(() => {
timeDiff.value = props.serverTime.getTime() - clientNow.getTime(); timeDiff.value = props.serverTime.getTime() - clientNow.getTime();
updateNow(); updateNow();
}); });
onUnmounted(() => {
if (timer !== undefined) {
clearTimeout(timer);
}
});
</script> </script>
+1
View File
@@ -7,6 +7,7 @@ export type ReservedCommandResponse = {
year: number; year: number;
month: number; month: number;
date: string; date: string;
clockMode: "realtime" | "manual";
turn: TurnObj[]; turn: TurnObj[];
autorun_limit: null | number; autorun_limit: null | number;
}; };
+3 -1
View File
@@ -34,6 +34,8 @@ export type MsgItem = {
delete?: number; delete?: number;
}; };
time: string; time: string;
deleteRemainingMilliseconds: number;
clockMode: "realtime" | "manual";
}; };
export type MsgPrintItem = MsgItem & { export type MsgPrintItem = MsgItem & {
@@ -77,4 +79,4 @@ export type MailboxItem = {
export type MabilboxListResponse = { export type MabilboxListResponse = {
result: true, result: true,
nation: MailboxItem[] nation: MailboxItem[]
} }
+2 -1
View File
@@ -7,6 +7,7 @@ export type ChiefResponse = {
month: number; month: number;
turnTerm: number; turnTerm: number;
date: string; date: string;
clockMode: "realtime" | "manual";
chiefList: Record< chiefList: Record<
number, number,
{ {
@@ -28,4 +29,4 @@ export type ChiefResponse = {
}[]; }[];
mapName: string, mapName: string,
unitSet: string, unitSet: string,
}; };
+2 -1
View File
@@ -30,4 +30,5 @@ export type VoteDetailResult = ValidResponse & {
comments: VoteComment[], comments: VoteComment[],
myVote: null|number[], myVote: null|number[],
userCnt: number, userCnt: number,
} isOpen: boolean,
}
+4 -5
View File
@@ -5,7 +5,6 @@ import axios from 'axios';
import { initTooltip } from "@/legacy/initTooltip"; import { initTooltip } from "@/legacy/initTooltip";
import { TemplateEngine } from '@util/TemplateEngine'; import { TemplateEngine } from '@util/TemplateEngine';
import type { InvalidResponse } from '@/defs'; import type { InvalidResponse } from '@/defs';
import { getDateTimeNow } from '@util/getDateTimeNow';
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest'; import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
import { loadPlugin as loadAdminPlugin } from '@/gateway/admin_server'; import { loadPlugin as loadAdminPlugin } from '@/gateway/admin_server';
import '@/gateway/common'; import '@/gateway/common';
@@ -108,6 +107,7 @@ type ReservedGameInfo = {
type GameInfo = { type GameInfo = {
isUnited: number, isUnited: number,
isOpen: boolean,
npcMode: '불가' | '가능' | '선택 생성', npcMode: '불가' | '가능' | '선택 생성',
year: number, year: number,
month: number, month: number,
@@ -175,7 +175,6 @@ async function Entrance_UpdateServer() {
async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) { async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) {
const $serverList = $('#server_list'); const $serverList = $('#server_list');
const now = getDateTimeNow();
const serverDetailInfoP: Record<string, Promise<ServerDetailResponse>> = {}; const serverDetailInfoP: Record<string, Promise<ServerDetailResponse>> = {};
@@ -239,7 +238,7 @@ async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) {
} else if (game.isUnited == 2) { } else if (game.isUnited == 2) {
$serverHtml.find('.n_country').html('§천하통일§'); $serverHtml.find('.n_country').html('§천하통일§');
$serverHtml.find('.server_date').html(`${game.starttime} <br>~ ${game.turntime}`); $serverHtml.find('.server_date').html(`${game.starttime} <br>~ ${game.turntime}`);
} else if (game.opentime <= now) { } else if (game.isOpen) {
$serverHtml.find('.n_country').html(`<${game.nationCnt}국 경쟁중>`); $serverHtml.find('.n_country').html(`<${game.nationCnt}국 경쟁중>`);
$serverHtml.find('.server_date').html(`${game.starttime} ~`); $serverHtml.find('.server_date').html(`${game.starttime} ~`);
} else { } else {
@@ -247,7 +246,7 @@ async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) {
$serverHtml.find('.server_date').html(`${game.starttime} ~`); $serverHtml.find('.server_date').html(`${game.starttime} ~`);
} }
if (game.opentime <= now) { if (game.isOpen) {
$serverHtml.append( $serverHtml.append(
TemplateEngine(serverTextInfo, game) TemplateEngine(serverTextInfo, game)
); );
@@ -294,4 +293,4 @@ async function Entrance_Logout() {
return; return;
} }
location.href = "../"; location.href = "../";
} }
+9 -14
View File
@@ -1,15 +1,11 @@
import $ from 'jquery'; import $ from 'jquery';
import type { InvalidResponse } from '@/defs'; import type { InvalidResponse } from '@/defs';
import { getDateTimeNow } from '@util/getDateTimeNow';
import axios from 'axios'; import axios from 'axios';
import { convertFormData } from '@util/convertFormData'; import { convertFormData } from '@util/convertFormData';
import { isBrightColor } from "@util/isBrightColor"; import { isBrightColor } from "@util/isBrightColor";
import { unwrap } from '@util/unwrap'; import { unwrap } from '@util/unwrap';
import _, { isError, isString } from 'lodash-es'; 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 { TemplateEngine } from '@util/TemplateEngine';
import { isNotNull } from '@util/isNotNull'; import { isNotNull } from '@util/isNotNull';
import { unwrap_any } from '@util/unwrap_any'; import { unwrap_any } from '@util/unwrap_any';
@@ -32,8 +28,8 @@ const messageTemplate = `<div
</div> </div>
<div class="msg_body"> <div class="msg_body">
<div class="msg_header"> <div class="msg_header">
<%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){%>
<button type="button" data-erase_until="<%last5min%>" class="btn btn btn-outline-warning btn-sm btn-delete-msg" style='float:right'>❌</button> <button type="button" data-erase_until="<%eraseUntil%>" class="btn btn btn-outline-warning btn-sm btn-delete-msg" style='float:right'>❌</button>
<%}%> <%}%>
<%if(msgType == 'private') {%> <%if(msgType == 'private') {%>
<%if(src.name == generalName){%> <%if(src.name == generalName){%>
@@ -101,8 +97,7 @@ type MsgPrintItem = MsgItem & {
nationType: 'local' | 'src' | 'dest'; nationType: 'local' | 'src' | 'dest';
myGeneralID: number; myGeneralID: number;
allowButton: boolean; allowButton: boolean;
last5min: string; eraseUntil: number;
now: string;
invalidType: 'msg_invalid' | 'msg_valid'; invalidType: 'msg_invalid' | 'msg_valid';
deletable: boolean; deletable: boolean;
src: MsgTarget & { colorType: 'bright' | 'dark' }, src: MsgTarget & { colorType: 'bright' | 'dark' },
@@ -217,7 +212,7 @@ async function showOldMsg(msgType: MsgType): Promise<MsgResponse> {
function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse { function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse {
function checkErasable(obj: MsgResponse) { function checkErasable(obj: MsgResponse) {
const now = getDateTimeNow(); const now = Date.now();
$('.btn-delete-msg').each(function () { $('.btn-delete-msg').each(function () {
const $btn = $(this); const $btn = $(this);
const eraseUntil = $btn.data('erase_until'); const eraseUntil = $btn.data('erase_until');
@@ -257,7 +252,6 @@ function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse {
let needRefreshLastContact = (msgType == 'private'); let needRefreshLastContact = (msgType == 'private');
const now = getDateTimeNow();
//list의 맨 앞이 가장 최신 메시지임. //list의 맨 앞이 가장 최신 메시지임.
const $msgs: JQuery<HTMLElement>[] = msgSource.map(function (msg) { const $msgs: JQuery<HTMLElement>[] = msgSource.map(function (msg) {
@@ -311,7 +305,9 @@ function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse {
allowButton = true; 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']; let invalidType: MsgPrintItem['invalidType'];
if (msg.option && msg.option.invalid) { if (msg.option && msg.option.invalid) {
invalidType = 'msg_invalid'; invalidType = 'msg_invalid';
@@ -336,9 +332,8 @@ function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse {
myGeneralID: unwrap(myGeneralID), myGeneralID: unwrap(myGeneralID),
src, src,
dest, dest,
now,
allowButton, allowButton,
last5min, eraseUntil,
invalidType, invalidType,
deletable, deletable,
defaultIcon, defaultIcon,
@@ -653,4 +648,4 @@ $(async function ($) {
const msgType = $this.data('msg_type'); const msgType = $this.data('msg_type');
void showOldMsg(msgType); void showOldMsg(msgType);
}) })
}); });
+13 -5
View File
@@ -42,8 +42,12 @@ type CardItem = {
type GeneralPoolResponse = { type GeneralPoolResponse = {
result: true, result: true,
pick: CardItem[], pick: CardItem[],
validUntil: string, validUntil: string,
} validForSeconds: number,
clockMode: "realtime" | "manual",
}
let logicalClockRunning = true;
declare const characterInfo: Record<string, { name: string, info: string }>; declare const characterInfo: Record<string, { name: string, info: string }>;
declare const hasGeneralID: number; declare const hasGeneralID: number;
@@ -185,7 +189,10 @@ async function buildGeneral(e: JQuery.Event) {
location.href = './'; location.href = './';
} }
function updateOutdateTimer() { function updateOutdateTimer() {
if (!logicalClockRunning) {
return;
}
const $validUntilText = $('#valid_until_text'); const $validUntilText = $('#valid_until_text');
const now = Date.now(); const now = Date.now();
const validUntil = $validUntilText.data('until'); const validUntil = $validUntilText.data('until');
@@ -204,10 +211,11 @@ function updateOutdateTimer() {
setTimeout(updateOutdateTimer, 1000); setTimeout(updateOutdateTimer, 1000);
} }
function printGenerals(value: GeneralPoolResponse) { function printGenerals(value: GeneralPoolResponse) {
logicalClockRunning = value.clockMode === "realtime";
$('.card_holder').empty(); $('.card_holder').empty();
$('#valid_until').show(); $('#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(); $('#outdate_token').hide();
const pick = value.pick.map(v => v);//XXX: 의도가 뭐였지? clone? const pick = value.pick.map(v => v);//XXX: 의도가 뭐였지? clone?
+24 -9
View File
@@ -68,9 +68,13 @@ type NPCToken = {
result: true, result: true,
pick: Record<number, NPCPick>, pick: Record<number, NPCPick>,
pickMoreFrom: string, pickMoreFrom: string,
pickMoreSeconds: number, pickMoreSeconds: number,
validUntil: string, validUntil: string,
} validForSeconds: number,
clockMode: "realtime" | "manual",
}
let logicalClockRunning = true;
const templateGeneralCard = const templateGeneralCard =
'<div class="general_card">\ '<div class="general_card">\
@@ -146,7 +150,10 @@ async function pickGeneral(this: HTMLElement, e: JQuery.Event) {
location.href = './'; location.href = './';
} }
function updateOutdateTimer() { function updateOutdateTimer() {
if (!logicalClockRunning) {
return;
}
const $validUntilText = $('#valid_until_text'); const $validUntilText = $('#valid_until_text');
const now = Date.now(); const now = Date.now();
const validUntil = $validUntilText.data('until'); const validUntil = $validUntilText.data('until');
@@ -165,8 +172,15 @@ function updateOutdateTimer() {
setTimeout(updateOutdateTimer, 1000); setTimeout(updateOutdateTimer, 1000);
} }
function updatePickMoreTimer() { function updatePickMoreTimer() {
const $btn = $('#btn_pick_more'); 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 now = Date.now();
const remain = ($btn.data('available') - now) / 1000; const remain = ($btn.data('available') - now) / 1000;
@@ -181,13 +195,14 @@ function updatePickMoreTimer() {
setTimeout(updatePickMoreTimer, 250); setTimeout(updatePickMoreTimer, 250);
} }
function printGenerals(value: NPCToken) { function printGenerals(value: NPCToken) {
logicalClockRunning = value.clockMode === "realtime";
$('.card_holder').empty(); $('.card_holder').empty();
$('#valid_until').show(); $('#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(); $('#outdate_token').hide();
const time = Date.now() + value.pickMoreSeconds * 1000; 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) { const pick = $.map(value.pick, function (value) {
return value; return value;
+5
View File
@@ -35,7 +35,12 @@ anchor에 고정하므로 표시 시각이 튀지 않습니다.
```bash ```bash
php scripts/verify-game-clock-engine.php --apply --engine-calls=2 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이 벽시계 때문에 이 검증기는 manual mode만 허용하고, 엔진 호출 전후 clock tick이 벽시계 때문에
변하지 않았는지와 마지막 처리 tick이 현재 tick을 넘지 않았는지 검사합니다. 변하지 않았는지와 마지막 처리 tick이 현재 tick을 넘지 않았는지 검사합니다.
`--until-unification`은 달력이나 DB 시각을 고쳐 쓰지 않고, 매월 manual clock만
정확히 다음 turn tick으로 옮긴 뒤 실제 `TurnExecutionHelper`를 반복 호출합니다.
각 호출에서 시계 고정과 처리 tick 상한을 재검사하고 `isunited=2|3`이 되지 않으면
성공으로 취급하지 않습니다. 반드시 격리 복제 DB에서 실행하세요.
+25
View File
@@ -4,6 +4,7 @@
declare(strict_types=1); declare(strict_types=1);
use sammo\DB; use sammo\DB;
use sammo\DTO\VoteInfo;
use sammo\GameClock; use sammo\GameClock;
use sammo\Json; use sammo\Json;
use sammo\KVStorage; use sammo\KVStorage;
@@ -269,6 +270,30 @@ try {
'last천도Trial', '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( $db->query(
'ALTER TABLE general ' 'ALTER TABLE general '
+118 -3
View File
@@ -19,16 +19,25 @@ $_SERVER['REQUEST_URI'] ??= '/cli/verify-game-clock-engine';
require dirname(__DIR__) . '/hwe/lib.php'; require dirname(__DIR__) . '/hwe/lib.php';
require dirname(__DIR__) . '/hwe/func.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'])) { 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); 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) { if ($engineCalls === false || $engineCalls < 1 || $engineCalls > 1000) {
throw new InvalidArgumentException('--engine-calls는 1..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(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
@@ -39,6 +48,112 @@ if ($clock->getMode() !== GameClock::MODE_MANUAL) {
$fixedNowTick = $clock->nowTick(); $fixedNowTick = $clock->nowTick();
$before = $gameStor->getValues(['year', 'month', 'turntime']); $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; $executedCount = 0;
for ($call = 0; $call < $engineCalls; $call++) { for ($call = 0; $call < $engineCalls; $call++) {
$executed = false; $executed = false;
+11 -1
View File
@@ -117,6 +117,11 @@ final class GameClock
return self::addTicks($this->anchorTick, $this->ticksBetween($this->wallAnchor, $this->wallNow())); 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 public function ticksFromSeconds(int|float $seconds): int
{ {
if (is_int($seconds)) { if (is_int($seconds)) {
@@ -207,6 +212,11 @@ final class GameClock
return TimeUtil::format($this->tickToDateTime($tick), $withFraction); return TimeUtil::format($this->tickToDateTime($tick), $withFraction);
} }
public function formatNow(bool $withFraction = false): string
{
return $this->formatTick($this->nowTick(), $withFraction);
}
public static function baseTimeForProjection( public static function baseTimeForProjection(
\DateTimeInterface $projectedTime, \DateTimeInterface $projectedTime,
int $tick, int $tick,
@@ -249,7 +259,7 @@ final class GameClock
public function advance(KVStorage $gameStor, int $deltaTick): int public function advance(KVStorage $gameStor, int $deltaTick): int
{ {
$nextTick = $this->nowTick() + $deltaTick; $nextTick = self::addTicks($this->nowTick(), $deltaTick);
$this->persistTick($gameStor, $nextTick); $this->persistTick($gameStor, $nextTick);
return $nextTick; return $nextTick;
} }
+15 -16
View File
@@ -239,13 +239,16 @@ class Session
$loginDate = $this->get($serverID.static::GAME_KEY_DATE); $loginDate = $this->get($serverID.static::GAME_KEY_DATE);
$generalID = $this->get($serverID.static::GAME_KEY_GENERAL_ID); $generalID = $this->get($serverID.static::GAME_KEY_GENERAL_ID);
$generalName = $this->get($serverID.static::GAME_KEY_GENERAL_NAME); $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 ( if (
$globalLoginDate < $loginDate && $globalLoginDate < $loginDate &&
$generalID && $generalName && $loginDate && $deadTime $generalID && $generalName && $loginDate && $deadTick
&& $loginDate + 1800 > $now && $deadTime > $now && $loginDate + 1800 > $wallNow && $deadTick > $gameNowTick
) { ) {
//로그인 정보는 30분간 유지한다. //로그인 정보는 30분간 유지한다.
if ($result !== null) { if ($result !== null) {
@@ -254,13 +257,10 @@ class Session
return $this; return $this;
} }
if ($generalID || $generalName || $loginDate || $deadTime) { if ($generalID || $generalName || $loginDate || $deadTick) {
$this->logoutGame(); $this->logoutGame();
} }
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$general = $db->queryFirstRow( $general = $db->queryFirstRow(
'SELECT `no`, `name`, `killturn`, `turntime` from general where `owner` = %i', 'SELECT `no`, `name`, `killturn`, `turntime` from general where `owner` = %i',
$userID $userID
@@ -272,16 +272,15 @@ class Session
return $this; return $this;
} }
$turnterm = $gameStor->turnterm;
$isUnited = $gameStor->isunited != 0; $isUnited = $gameStor->isunited != 0;
$generalID = $general['no']; $generalID = $general['no'];
$generalName = $general['name']; $generalName = $general['name'];
$nextTurn = new \DateTime($general['turntime']); $deadTick = GameClock::addTicks(
$nextTurn = $nextTurn->getTimestamp(); Util::toInt($general['turntime']),
Util::toInt($general['killturn']) * GameClock::TICKS_PER_TURN,
$deadTime = $nextTurn + $general['killturn'] * $turnterm; );
if ($deadTime < $now && !$isUnited) { if ($deadTick < $gameNowTick && !$isUnited) {
$locked = $db->queryFirstField('SELECT plock FROM plock WHERE `type` = "GAME" LIMIT 1'); $locked = $db->queryFirstField('SELECT plock FROM plock WHERE `type` = "GAME" LIMIT 1');
if (!$locked) { if (!$locked) {
if ($result !== null) { 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_ID, $generalID);
$this->set($serverID.static::GAME_KEY_GENERAL_NAME, $generalName); $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; return $this;
} }
+89 -2
View File
@@ -18,6 +18,7 @@ final class GameClockBoundaryTest extends TestCase
'/\bNOW\s*\(/i', '/\bNOW\s*\(/i',
'/\bCURRENT_TIMESTAMP\b/i', '/\bCURRENT_TIMESTAMP\b/i',
'/\bCURDATE\s*\(/i', '/\bCURDATE\s*\(/i',
'/\btime\s*\(/i',
] as $pattern) { ] as $pattern) {
self::assertDoesNotMatchRegularExpression($pattern, $source, $relativePath); self::assertDoesNotMatchRegularExpression($pattern, $source, $relativePath);
} }
@@ -25,7 +26,7 @@ final class GameClockBoundaryTest extends TestCase
public static function gameSchedulingFiles(): array public static function gameSchedulingFiles(): array
{ {
return array_map(static fn (string $path): array => [$path], [ $paths = [
'hwe/sammo/TurnExecutionHelper.php', 'hwe/sammo/TurnExecutionHelper.php',
'hwe/sammo/Auction.php', 'hwe/sammo/Auction.php',
'hwe/sammo/AuctionBasicResource.php', 'hwe/sammo/AuctionBasicResource.php',
@@ -36,7 +37,43 @@ final class GameClockBoundaryTest extends TestCase
'hwe/sammo/AbsFromUserPool.php', 'hwe/sammo/AbsFromUserPool.php',
'hwe/sammo/GeneralPool/RandomNameGeneral.php', 'hwe/sammo/GeneralPool/RandomNameGeneral.php',
'hwe/sammo/API/General/DieOnPrestart.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 public function testTickSchemaDoesNotUseDatabaseDefaultsForGameSchedules(): void
@@ -55,5 +92,55 @@ final class GameClockBoundaryTest extends TestCase
] as $expected) { ] as $expected) {
self::assertStringContainsString($expected, $schema); 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'));
}
} }
+8
View File
@@ -5,6 +5,7 @@ namespace sammo;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/../src/sammo/GameClock.php'; require_once __DIR__ . '/../src/sammo/GameClock.php';
require_once __DIR__ . '/../hwe/sammo/TurnExecutionHelper.php';
final class GameClockTest extends TestCase final class GameClockTest extends TestCase
{ {
@@ -58,6 +59,7 @@ final class GameClockTest extends TestCase
); );
self::assertSame(123_456, $clock->nowTick()); self::assertSame(123_456, $clock->nowTick());
self::assertSame($clock->formatTick(123_456), $clock->formatNow());
self::assertFalse($wallRead); self::assertFalse($wallRead);
} }
@@ -96,4 +98,10 @@ final class GameClockTest extends TestCase
$this->expectException(\OverflowException::class); $this->expectException(\OverflowException::class);
GameClock::addTicks(GameClock::MAX_SAFE_TICK, 1); 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));
}
} }
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace sammo;
use PHPUnit\Framework\TestCase;
use sammo\DTO\VoteInfo;
require_once __DIR__ . '/../src/sammo/GameClock.php';
require_once __DIR__ . '/../hwe/sammo/DTO/VoteInfo.php';
final class VoteGameClockTest extends TestCase
{
public function testLegacyDatesAreConvertedToStableTicksAndProjectedAgain(): void
{
$base = new \DateTimeImmutable('2035-01-01 00:00:00.000000');
$clock = new GameClock($base, 60, 0, GameClock::MODE_MANUAL, $base);
$raw = [
'id' => 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']);
}
}