fix: complete logical clock wall-time isolation
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
]);
|
||||
]);
|
||||
|
||||
@@ -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()
|
||||
]);
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
]);
|
||||
|
||||
@@ -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'
|
||||
]);
|
||||
]);
|
||||
|
||||
@@ -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'
|
||||
]);
|
||||
]);
|
||||
|
||||
@@ -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()
|
||||
]);
|
||||
]);
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
@@ -290,7 +290,7 @@ function do추방(General $general, int $myOfficerLevel): ?string
|
||||
$src,
|
||||
$src,
|
||||
$str,
|
||||
new \DateTime(),
|
||||
Message::gameNow(),
|
||||
new \DateTime('9999-12-31'),
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'),
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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 '설문조사가 종료되었습니다.';
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -3727,7 +3727,7 @@ class GeneralAI
|
||||
$src,
|
||||
$src,
|
||||
$general->getVar('npcmsg'),
|
||||
new \DateTime(),
|
||||
Message::gameNow(),
|
||||
new \DateTime('9999-12-31'),
|
||||
[]
|
||||
);
|
||||
|
||||
+24
-5
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ class RaiseInvaderMessage extends Message
|
||||
$srcTarget = MessageTarget::buildSystemTarget();
|
||||
$destTarget = MessageTarget::buildQuick($destGeneralID);
|
||||
if ($date === null) {
|
||||
$date = new \DateTime();
|
||||
$date = Message::gameNow();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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
@@ -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>();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,6 +7,7 @@ export type ReservedCommandResponse = {
|
||||
year: number;
|
||||
month: number;
|
||||
date: string;
|
||||
clockMode: "realtime" | "manual";
|
||||
turn: TurnObj[];
|
||||
autorun_limit: null | number;
|
||||
};
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -30,4 +30,5 @@ export type VoteDetailResult = ValidResponse & {
|
||||
comments: VoteComment[],
|
||||
myVote: null|number[],
|
||||
userCnt: number,
|
||||
}
|
||||
isOpen: boolean,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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에서 실행하세요.
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user