fix: complete logical clock wall-time isolation

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